fix showing images on client
This commit is contained in:
parent
cc2810bbf9
commit
273b20a76c
@ -148,6 +148,37 @@ class ApiClient {
|
|||||||
return this.makeRequest('DELETE', `/images/${imageId}`);
|
return this.makeRequest('DELETE', `/images/${imageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download image with authentication and return blob URL
|
||||||
|
async downloadImageBlob(imageId) {
|
||||||
|
this.updateConfig();
|
||||||
|
|
||||||
|
if (!this.baseUrl) {
|
||||||
|
throw new Error('API not configured. Please set API base URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/api/v1/images/${imageId}/download`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders(false) // Don't include Content-Type for binary data
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Image download failed: ${url}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search API
|
// Search API
|
||||||
async searchImages(query, similarityThreshold = 0.7, maxResults = 20, tags = null) {
|
async searchImages(query, similarityThreshold = 0.7, maxResults = 20, tags = null) {
|
||||||
const searchData = {
|
const searchData = {
|
||||||
|
|||||||
@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
|
let imageBlobCache = new Map(); // Cache for blob URLs
|
||||||
|
|
||||||
|
// Load image with authentication and return blob URL
|
||||||
|
async function loadImageBlob(imageId) {
|
||||||
|
// Check cache first
|
||||||
|
if (imageBlobCache.has(imageId)) {
|
||||||
|
return imageBlobCache.get(imageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blobUrl = await apiClient.downloadImageBlob(imageId);
|
||||||
|
imageBlobCache.set(imageId, blobUrl);
|
||||||
|
return blobUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load image ${imageId}:`, error);
|
||||||
|
throw error; // Let the caller handle the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up blob URLs to prevent memory leaks
|
||||||
|
function cleanupBlobCache() {
|
||||||
|
for (const [imageId, blobUrl] of imageBlobCache.entries()) {
|
||||||
|
if (blobUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageBlobCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Load images with pagination
|
// Load images with pagination
|
||||||
async function loadImages(page = 1, tags = null) {
|
async function loadImages(page = 1, tags = null) {
|
||||||
@ -29,7 +57,7 @@ async function loadImages(page = 1, tags = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display images in grid
|
// Display images in grid
|
||||||
function displayImages(images) {
|
async function displayImages(images) {
|
||||||
const container = document.getElementById('imagesContainer');
|
const container = document.getElementById('imagesContainer');
|
||||||
|
|
||||||
if (!images || images.length === 0) {
|
if (!images || images.length === 0) {
|
||||||
@ -46,12 +74,20 @@ function displayImages(images) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create image cards with placeholder images first
|
||||||
const imagesHtml = images.map(image => `
|
const imagesHtml = images.map(image => `
|
||||||
<div class="image-card card">
|
<div class="image-card card">
|
||||||
<img src="${image.public_url || '/placeholder-image.png'}"
|
<div class="image-container" style="position: relative; height: 200px; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<img id="img-${image.id}"
|
||||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InRyYW5zcGFyZW50Ii8+PC9zdmc+"
|
||||||
alt="${escapeHtml(image.description || 'Image')}"
|
alt="${escapeHtml(image.description || 'Image')}"
|
||||||
onclick="viewImage('${image.id}')"
|
onclick="viewImage('${image.id}')"
|
||||||
style="cursor: pointer;">
|
style="cursor: pointer; opacity: 0; width: 100%; height: 200px; object-fit: cover; position: absolute; top: 0; left: 0;">
|
||||||
|
<div class="loading-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2;">
|
||||||
|
<div class="loading-spinner large"></div>
|
||||||
|
<div class="mt-2 text-muted small">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">${escapeHtml(truncateText(image.description || 'Untitled', 50))}</h6>
|
<h6 class="card-title">${escapeHtml(truncateText(image.description || 'Untitled', 50))}</h6>
|
||||||
<p class="card-text small text-muted">
|
<p class="card-text small text-muted">
|
||||||
@ -78,6 +114,37 @@ function displayImages(images) {
|
|||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
container.innerHTML = `<div class="image-grid">${imagesHtml}</div>`;
|
container.innerHTML = `<div class="image-grid">${imagesHtml}</div>`;
|
||||||
|
|
||||||
|
// Load actual images asynchronously
|
||||||
|
for (const image of images) {
|
||||||
|
loadImageBlob(image.id).then(blobUrl => {
|
||||||
|
const imgElement = document.getElementById(`img-${image.id}`);
|
||||||
|
const loadingOverlay = imgElement.parentElement.querySelector('.loading-overlay');
|
||||||
|
|
||||||
|
if (imgElement) {
|
||||||
|
imgElement.src = blobUrl;
|
||||||
|
imgElement.style.opacity = '1';
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(`Failed to load image ${image.id}:`, error);
|
||||||
|
const imgElement = document.getElementById(`img-${image.id}`);
|
||||||
|
const loadingOverlay = imgElement.parentElement.querySelector('.loading-overlay');
|
||||||
|
|
||||||
|
if (imgElement && loadingOverlay) {
|
||||||
|
// Show error state instead of broken image
|
||||||
|
loadingOverlay.innerHTML = `
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-2x mb-2"></i>
|
||||||
|
<div class="small">Failed to load</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
imgElement.style.display = 'none'; // Hide the img element completely
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display pagination
|
// Display pagination
|
||||||
@ -234,12 +301,31 @@ async function uploadImage() {
|
|||||||
// View image details
|
// View image details
|
||||||
async function viewImage(imageId) {
|
async function viewImage(imageId) {
|
||||||
try {
|
try {
|
||||||
|
// Close any existing modals first
|
||||||
|
const existingModals = ['viewImageModal', 'editImageModal'];
|
||||||
|
existingModals.forEach(modalId => {
|
||||||
|
const existingModal = document.getElementById(modalId);
|
||||||
|
if (existingModal) {
|
||||||
|
const bsModal = bootstrap.Modal.getInstance(existingModal);
|
||||||
|
if (bsModal) {
|
||||||
|
bsModal.hide();
|
||||||
|
}
|
||||||
|
removeModal(modalId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const image = await apiClient.getImage(imageId);
|
const image = await apiClient.getImage(imageId);
|
||||||
|
|
||||||
|
// Load the image with authentication
|
||||||
|
const imageSrc = await loadImageBlob(imageId);
|
||||||
|
|
||||||
|
// Use unique modal ID to prevent conflicts
|
||||||
|
const modalId = `viewImageModal-${imageId}`;
|
||||||
|
|
||||||
const modalBody = `
|
const modalBody = `
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
<img src="${imageSrc}" class="img-fluid rounded"
|
||||||
style="max-height: 400px;">
|
style="max-height: 400px;" alt="${escapeHtml(image.description || 'Image')}">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -271,7 +357,14 @@ async function viewImage(imageId) {
|
|||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const modal = createModal('viewImageModal', 'Image Details', modalBody, modalFooter);
|
const modal = createModal(modalId, 'Image Details', modalBody, modalFooter);
|
||||||
|
|
||||||
|
// Add cleanup when modal is hidden
|
||||||
|
const modalElement = document.getElementById(modalId);
|
||||||
|
modalElement.addEventListener('hidden.bs.modal', () => {
|
||||||
|
removeModal(modalId);
|
||||||
|
});
|
||||||
|
|
||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -282,21 +375,37 @@ async function viewImage(imageId) {
|
|||||||
// Edit image
|
// Edit image
|
||||||
async function editImage(imageId) {
|
async function editImage(imageId) {
|
||||||
try {
|
try {
|
||||||
|
// Close any existing modals first
|
||||||
|
const existingModals = document.querySelectorAll('.modal');
|
||||||
|
existingModals.forEach(modal => {
|
||||||
|
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||||
|
if (bsModal) {
|
||||||
|
bsModal.hide();
|
||||||
|
}
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
const image = await apiClient.getImage(imageId);
|
const image = await apiClient.getImage(imageId);
|
||||||
|
|
||||||
|
// Load the image with authentication
|
||||||
|
const imageSrc = await loadImageBlob(imageId);
|
||||||
|
|
||||||
|
// Use unique modal ID to prevent conflicts
|
||||||
|
const modalId = `editImageModal-${imageId}`;
|
||||||
|
|
||||||
const modalBody = `
|
const modalBody = `
|
||||||
<form id="editImageForm">
|
<form id="editImageForm-${imageId}">
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
<img src="${imageSrc}" class="img-fluid rounded"
|
||||||
style="max-height: 200px;">
|
style="max-height: 200px;" alt="${escapeHtml(image.description || 'Image')}">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="editDescription" class="form-label">Description</label>
|
<label for="editDescription-${imageId}" class="form-label">Description</label>
|
||||||
<textarea class="form-control" id="editDescription" rows="3">${escapeHtml(image.description || '')}</textarea>
|
<textarea class="form-control" id="editDescription-${imageId}" rows="3">${escapeHtml(image.description || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="editTags" class="form-label">Tags</label>
|
<label for="editTags-${imageId}" class="form-label">Tags</label>
|
||||||
<input type="text" class="form-control" id="editTags"
|
<input type="text" class="form-control" id="editTags-${imageId}"
|
||||||
value="${image.tags ? image.tags.join(', ') : ''}">
|
value="${image.tags ? image.tags.join(', ') : ''}">
|
||||||
<div class="form-text">Enter tags separated by commas</div>
|
<div class="form-text">Enter tags separated by commas</div>
|
||||||
</div>
|
</div>
|
||||||
@ -310,7 +419,14 @@ async function editImage(imageId) {
|
|||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const modal = createModal('editImageModal', 'Edit Image', modalBody, modalFooter);
|
const modal = createModal(modalId, 'Edit Image', modalBody, modalFooter);
|
||||||
|
|
||||||
|
// Add cleanup when modal is hidden
|
||||||
|
const modalElement = document.getElementById(modalId);
|
||||||
|
modalElement.addEventListener('hidden.bs.modal', () => {
|
||||||
|
removeModal(modalId);
|
||||||
|
});
|
||||||
|
|
||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -320,11 +436,11 @@ async function editImage(imageId) {
|
|||||||
|
|
||||||
// Save image changes
|
// Save image changes
|
||||||
async function saveImageChanges(imageId) {
|
async function saveImageChanges(imageId) {
|
||||||
const description = document.getElementById('editDescription').value.trim();
|
const description = document.getElementById(`editDescription-${imageId}`).value.trim();
|
||||||
const tagsInput = document.getElementById('editTags').value.trim();
|
const tagsInput = document.getElementById(`editTags-${imageId}`).value.trim();
|
||||||
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
||||||
|
|
||||||
const saveButton = document.querySelector('#editImageModal .btn-primary');
|
const saveButton = document.querySelector(`#editImageModal-${imageId} .btn-primary`);
|
||||||
setLoadingState(saveButton);
|
setLoadingState(saveButton);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -336,8 +452,8 @@ async function saveImageChanges(imageId) {
|
|||||||
showAlert('Image updated successfully!', 'success');
|
showAlert('Image updated successfully!', 'success');
|
||||||
|
|
||||||
// Close modal and refresh images
|
// Close modal and refresh images
|
||||||
bootstrap.Modal.getInstance(document.getElementById('editImageModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById(`editImageModal-${imageId}`)).hide();
|
||||||
removeModal('editImageModal');
|
removeModal(`editImageModal-${imageId}`);
|
||||||
loadImages(currentPage);
|
loadImages(currentPage);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -355,12 +471,15 @@ function deleteImage(imageId) {
|
|||||||
showAlert('Image deleted successfully!', 'success');
|
showAlert('Image deleted successfully!', 'success');
|
||||||
loadImages(currentPage);
|
loadImages(currentPage);
|
||||||
|
|
||||||
// Close any open modals
|
// Close any open modals for this image
|
||||||
const modals = ['viewImageModal', 'editImageModal'];
|
const modalsToClose = [`viewImageModal-${imageId}`, `editImageModal-${imageId}`];
|
||||||
modals.forEach(modalId => {
|
modalsToClose.forEach(modalId => {
|
||||||
const modalElement = document.getElementById(modalId);
|
const modalElement = document.getElementById(modalId);
|
||||||
if (modalElement) {
|
if (modalElement) {
|
||||||
bootstrap.Modal.getInstance(modalElement)?.hide();
|
const bsModal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
if (bsModal) {
|
||||||
|
bsModal.hide();
|
||||||
|
}
|
||||||
removeModal(modalId);
|
removeModal(modalId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -44,7 +44,7 @@ async function performSearch() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await apiClient.searchImages(query, threshold, maxResults);
|
const results = await apiClient.searchImages(query, threshold, maxResults);
|
||||||
displaySearchResults(results, query);
|
await displaySearchResults(results, query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error, 'searching images');
|
handleApiError(error, 'searching images');
|
||||||
resultsContainer.innerHTML = `
|
resultsContainer.innerHTML = `
|
||||||
@ -57,7 +57,7 @@ async function performSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display search results
|
// Display search results
|
||||||
function displaySearchResults(results, query) {
|
async function displaySearchResults(results, query) {
|
||||||
const container = document.getElementById('searchResults');
|
const container = document.getElementById('searchResults');
|
||||||
|
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
@ -65,17 +65,13 @@ function displaySearchResults(results, query) {
|
|||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
||||||
<h4>No results found</h4>
|
<h4>No results found</h4>
|
||||||
<p class="text-muted">Try adjusting your search query or similarity threshold.</p>
|
<p class="text-muted">Try adjusting your search query or similarity threshold</p>
|
||||||
<div class="mt-3">
|
|
||||||
<button class="btn btn-outline-primary" onclick="document.getElementById('searchQuery').focus()">
|
|
||||||
<i class="fas fa-edit me-1"></i>Refine Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create results HTML with placeholder images first
|
||||||
const resultsHtml = `
|
const resultsHtml = `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4>Search Results</h4>
|
<h4>Search Results</h4>
|
||||||
@ -86,11 +82,17 @@ function displaySearchResults(results, query) {
|
|||||||
<div class="col-md-6 col-lg-4 mb-4">
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
<div class="card search-result h-100">
|
<div class="card search-result h-100">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<img src="${result.image.public_url || '/placeholder-image.png'}"
|
<div class="image-container" style="position: relative; height: 200px;">
|
||||||
|
<img id="search-img-${result.image.id}"
|
||||||
|
src="/placeholder-image.png"
|
||||||
class="card-img-top"
|
class="card-img-top"
|
||||||
alt="${escapeHtml(result.image.description || 'Image')}"
|
alt="${escapeHtml(result.image.description || 'Image')}"
|
||||||
style="height: 200px; object-fit: cover; cursor: pointer;"
|
style="height: 200px; object-fit: cover; cursor: pointer; opacity: 0.5;"
|
||||||
onclick="viewImage('${result.image.id}')">
|
onclick="viewImage('${result.image.id}')">
|
||||||
|
<div class="loading-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="position-absolute top-0 end-0 m-2">
|
<div class="position-absolute top-0 end-0 m-2">
|
||||||
<span class="badge bg-primary similarity-score">
|
<span class="badge bg-primary similarity-score">
|
||||||
${Math.round(result.similarity * 100)}% match
|
${Math.round(result.similarity * 100)}% match
|
||||||
@ -134,6 +136,36 @@ function displaySearchResults(results, query) {
|
|||||||
|
|
||||||
container.innerHTML = resultsHtml;
|
container.innerHTML = resultsHtml;
|
||||||
|
|
||||||
|
// Load actual images asynchronously (need to import loadImageBlob from images.js)
|
||||||
|
for (const result of results) {
|
||||||
|
if (typeof loadImageBlob === 'function') {
|
||||||
|
loadImageBlob(result.image.id).then(blobUrl => {
|
||||||
|
const imgElement = document.getElementById(`search-img-${result.image.id}`);
|
||||||
|
const loadingOverlay = imgElement?.parentElement.querySelector('.loading-overlay');
|
||||||
|
|
||||||
|
if (imgElement) {
|
||||||
|
imgElement.src = blobUrl;
|
||||||
|
imgElement.style.opacity = '1';
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(`Failed to load search image ${result.image.id}:`, error);
|
||||||
|
const imgElement = document.getElementById(`search-img-${result.image.id}`);
|
||||||
|
const loadingOverlay = imgElement?.parentElement.querySelector('.loading-overlay');
|
||||||
|
|
||||||
|
if (imgElement) {
|
||||||
|
imgElement.src = '/placeholder-image.png';
|
||||||
|
imgElement.style.opacity = '1';
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add search refinement options
|
// Add search refinement options
|
||||||
addSearchRefinementOptions(query, results);
|
addSearchRefinementOptions(query, results);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,12 @@ function showPage(pageId) {
|
|||||||
try {
|
try {
|
||||||
console.log('=== showPage called with pageId:', pageId, '===');
|
console.log('=== showPage called with pageId:', pageId, '===');
|
||||||
|
|
||||||
|
// Clean up blob URLs when navigating away from images or search pages
|
||||||
|
if (typeof cleanupBlobCache === 'function' &&
|
||||||
|
(app.currentPage === 'images' || app.currentPage === 'search')) {
|
||||||
|
cleanupBlobCache();
|
||||||
|
}
|
||||||
|
|
||||||
// Hide all pages
|
// Hide all pages
|
||||||
const pages = document.querySelectorAll('.page');
|
const pages = document.querySelectorAll('.page');
|
||||||
console.log('Found pages:', pages.length);
|
console.log('Found pages:', pages.length);
|
||||||
@ -40,6 +46,7 @@ function showPage(pageId) {
|
|||||||
if (window.SeReactApp) {
|
if (window.SeReactApp) {
|
||||||
window.SeReactApp.currentPage = pageId;
|
window.SeReactApp.currentPage = pageId;
|
||||||
}
|
}
|
||||||
|
app.currentPage = pageId; // Also update the local app state
|
||||||
|
|
||||||
// Load page data if needed
|
// Load page data if needed
|
||||||
console.log('About to load page data for:', pageId);
|
console.log('About to load page data for:', pageId);
|
||||||
|
|||||||
@ -99,6 +99,15 @@ body {
|
|||||||
animation: spin 1s ease-in-out infinite;
|
animation: spin 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Large loading spinner for image loading */
|
||||||
|
.loading-spinner.large {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-width: 4px;
|
||||||
|
border-color: rgba(13, 110, 253, 0.3);
|
||||||
|
border-top-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|||||||
49
debug_api_key_deployment.py
Normal file
49
debug_api_key_deployment.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Debug script to test API key hashing with deployment configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def hash_api_key_manual(api_key: str, secret: str) -> str:
|
||||||
|
"""
|
||||||
|
Manual implementation of API key hashing to test
|
||||||
|
"""
|
||||||
|
return hmac.new(
|
||||||
|
secret.encode(),
|
||||||
|
api_key.encode(),
|
||||||
|
hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
def test_api_key_hashing():
|
||||||
|
"""Test API key hashing with different secrets"""
|
||||||
|
|
||||||
|
# The new API key from your seeding output
|
||||||
|
test_api_key = "uZBVUEku.7332d85bf6cf2618ad76bca46c2f8125"
|
||||||
|
|
||||||
|
# Test with different possible secrets
|
||||||
|
secrets_to_test = [
|
||||||
|
"super-secret-key-for-development-only", # Default from config.py
|
||||||
|
"development-secret-key-do-not-use-in-production", # Your .env value
|
||||||
|
]
|
||||||
|
|
||||||
|
print("API Key Hashing Debug")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Test API Key: {test_api_key}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for secret in secrets_to_test:
|
||||||
|
hashed_key = hash_api_key_manual(test_api_key, secret)
|
||||||
|
print(f"Secret: {secret}")
|
||||||
|
print(f"Hash: {hashed_key}")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("The deployment should be using one of these hashes in the database.")
|
||||||
|
print("Check your Cloud Run environment variables to confirm which secret is being used.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_api_key_hashing()
|
||||||
@ -144,6 +144,11 @@ resource "google_cloud_run_service" "sereact" {
|
|||||||
name = "LOG_LEVEL"
|
name = "LOG_LEVEL"
|
||||||
value = "INFO"
|
value = "INFO"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "API_KEY_SECRET"
|
||||||
|
value = "super-secret-key-for-development-only"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.10.1",
|
"terraform_version": "1.10.1",
|
||||||
"serial": 417,
|
"serial": 425,
|
||||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"cloud_function_name": {
|
"cloud_function_name": {
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"effective_annotations": {
|
"effective_annotations": {
|
||||||
"run.googleapis.com/ingress": "all",
|
"run.googleapis.com/ingress": "all",
|
||||||
"run.googleapis.com/ingress-status": "all",
|
"run.googleapis.com/ingress-status": "all",
|
||||||
"run.googleapis.com/operation-id": "7869f742-fe94-42d0-8d82-a1462681980d",
|
"run.googleapis.com/operation-id": "7a9a82f0-4bc9-4fe6-af40-ddee543c0536",
|
||||||
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
||||||
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
||||||
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
||||||
@ -182,14 +182,14 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"generation": 1,
|
"generation": 1,
|
||||||
"labels": {},
|
"labels": null,
|
||||||
"namespace": "gen-lang-client-0424120530",
|
"namespace": "gen-lang-client-0424120530",
|
||||||
"resource_version": "AAY16UbSm4k",
|
"resource_version": "AAY18uNZfE8",
|
||||||
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
||||||
"terraform_labels": {
|
"terraform_labels": {
|
||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"uid": "d5532269-ab10-4b77-b90f-698306bf0919"
|
"uid": "dfae23a0-4b71-40aa-baa8-64f5b842501a"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "sereact",
|
"name": "sereact",
|
||||||
@ -216,14 +216,14 @@
|
|||||||
"type": "RoutesReady"
|
"type": "RoutesReady"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"latest_created_revision_name": "sereact-00001-9rv",
|
"latest_created_revision_name": "sereact-00001-58h",
|
||||||
"latest_ready_revision_name": "sereact-00001-9rv",
|
"latest_ready_revision_name": "sereact-00001-58h",
|
||||||
"observed_generation": 1,
|
"observed_generation": 1,
|
||||||
"traffic": [
|
"traffic": [
|
||||||
{
|
{
|
||||||
"latest_revision": true,
|
"latest_revision": true,
|
||||||
"percent": 100,
|
"percent": 100,
|
||||||
"revision_name": "sereact-00001-9rv",
|
"revision_name": "sereact-00001-58h",
|
||||||
"tag": "",
|
"tag": "",
|
||||||
"url": ""
|
"url": ""
|
||||||
}
|
}
|
||||||
@ -256,9 +256,14 @@
|
|||||||
"container_concurrency": 80,
|
"container_concurrency": 80,
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": null,
|
||||||
"command": [],
|
"command": null,
|
||||||
"env": [
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "API_KEY_SECRET",
|
||||||
|
"value": "super-secret-key-for-development-only",
|
||||||
|
"value_from": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "FIRESTORE_DATABASE_NAME",
|
"name": "FIRESTORE_DATABASE_NAME",
|
||||||
"value": "sereact-imagedb",
|
"value": "sereact-imagedb",
|
||||||
@ -332,7 +337,7 @@
|
|||||||
"cpu": "1",
|
"cpu": "1",
|
||||||
"memory": "1Gi"
|
"memory": "1Gi"
|
||||||
},
|
},
|
||||||
"requests": {}
|
"requests": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"startup_probe": [
|
"startup_probe": [
|
||||||
@ -354,7 +359,7 @@
|
|||||||
"working_dir": ""
|
"working_dir": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_selector": {},
|
"node_selector": null,
|
||||||
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"serving_state": "",
|
"serving_state": "",
|
||||||
"timeout_seconds": 300,
|
"timeout_seconds": 300,
|
||||||
@ -435,7 +440,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16UdHJ00=",
|
"etag": "BwY18uO4z7A=",
|
||||||
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
|
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
|
||||||
"location": "us-central1",
|
"location": "us-central1",
|
||||||
"member": "allUsers",
|
"member": "allUsers",
|
||||||
@ -469,7 +474,7 @@
|
|||||||
"automatic_update_policy": [
|
"automatic_update_policy": [
|
||||||
{}
|
{}
|
||||||
],
|
],
|
||||||
"build": "projects/761163285547/locations/us-central1/builds/7b34015c-eb2f-4a80-ac64-b7d3355173ac",
|
"build": "projects/761163285547/locations/us-central1/builds/47fecac1-7233-42af-ad0e-2a5a9c66e282",
|
||||||
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
||||||
"entry_point": "process_image_embedding",
|
"entry_point": "process_image_embedding",
|
||||||
"environment_variables": {},
|
"environment_variables": {},
|
||||||
@ -526,6 +531,7 @@
|
|||||||
"GOOGLE_CLOUD_PROJECT": "gen-lang-client-0424120530",
|
"GOOGLE_CLOUD_PROJECT": "gen-lang-client-0424120530",
|
||||||
"LOG_EXECUTION_ID": "true",
|
"LOG_EXECUTION_ID": "true",
|
||||||
"LOG_LEVEL": "INFO",
|
"LOG_LEVEL": "INFO",
|
||||||
|
"PROJECT_ID": "gen-lang-client-0424120530",
|
||||||
"QDRANT_API_KEY": "",
|
"QDRANT_API_KEY": "",
|
||||||
"QDRANT_COLLECTION": "image_vectors",
|
"QDRANT_COLLECTION": "image_vectors",
|
||||||
"QDRANT_HOST": "34.71.6.1",
|
"QDRANT_HOST": "34.71.6.1",
|
||||||
@ -553,7 +559,7 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"timeouts": null,
|
"timeouts": null,
|
||||||
"update_time": "2025-05-24T22:39:42.051374046Z",
|
"update_time": "2025-05-25T09:26:49.473142736Z",
|
||||||
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
@ -803,12 +809,6 @@
|
|||||||
"zone": "us-central1-a"
|
"zone": "us-central1-a"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "get_attr",
|
|
||||||
"value": "metadata_startup_script"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
@ -842,6 +842,12 @@
|
|||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
"value": "disk_encryption_key_rsa"
|
"value": "disk_encryption_key_rsa"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "get_attr",
|
||||||
|
"value": "metadata_startup_script"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiNiJ9",
|
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiNiJ9",
|
||||||
@ -869,8 +875,8 @@
|
|||||||
"database_edition": "STANDARD",
|
"database_edition": "STANDARD",
|
||||||
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
||||||
"deletion_policy": "ABANDON",
|
"deletion_policy": "ABANDON",
|
||||||
"earliest_version_time": "2025-05-24T21:38:29.341046Z",
|
"earliest_version_time": "2025-05-25T08:59:14.308781Z",
|
||||||
"etag": "IOGow/2VvY0DMKrW4vCEvY0D",
|
"etag": "IMCn9pGuvo0DMKrW4vCEvY0D",
|
||||||
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
||||||
"key_prefix": "",
|
"key_prefix": "",
|
||||||
"location_id": "us-central1",
|
"location_id": "us-central1",
|
||||||
@ -901,7 +907,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -925,7 +931,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -949,7 +955,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -973,7 +979,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -1518,7 +1524,7 @@
|
|||||||
"md5hash": "Ru+hruU4bi8kS1lyicfEug==",
|
"md5hash": "Ru+hruU4bi8kS1lyicfEug==",
|
||||||
"md5hexhash": "46efa1aee5386e2f244b597289c7c4ba",
|
"md5hexhash": "46efa1aee5386e2f244b597289c7c4ba",
|
||||||
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip?generation=1748126317190781\u0026alt=media",
|
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip?generation=1748126317190781\u0026alt=media",
|
||||||
"metadata": null,
|
"metadata": {},
|
||||||
"name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
"name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
"output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"retention": [],
|
"retention": [],
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.10.1",
|
"terraform_version": "1.10.1",
|
||||||
"serial": 410,
|
"serial": 422,
|
||||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"cloud_function_name": {
|
"cloud_function_name": {
|
||||||
@ -98,16 +98,16 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"exclude_symlink_directories": null,
|
"exclude_symlink_directories": null,
|
||||||
"excludes": null,
|
"excludes": null,
|
||||||
"id": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
|
"id": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
|
||||||
"output_base64sha256": "0wAfDV7tH41jEspQ3LBLvIEnrQ6XU2aEtK2GWsMdyCA=",
|
"output_base64sha256": "cXx9sC1kIbTDG7BlKAtf3FUasHLZ/wZPzVoyBvt8p9Q=",
|
||||||
"output_base64sha512": "glsNAiHzSTOy9mGDckkSyDJhBVFDtLh8Xr6+hSxtCT8nok9qNGO+61iTRLU42OPxaPS/BrbDAXJeT86F3riefA==",
|
"output_base64sha512": "dcntRZ4Hz4dfBBj7YVsTzx+SEAqCXZCD8TAAh8cr5xa3uT2Lsmtf8zpxpyQEeMlsCNaF8dUohGQ7BD9LJYigPw==",
|
||||||
"output_file_mode": null,
|
"output_file_mode": null,
|
||||||
"output_md5": "a5d3a7fe131c972bf8d0edf309545042",
|
"output_md5": "46efa1aee5386e2f244b597289c7c4ba",
|
||||||
"output_path": "./function-source.zip",
|
"output_path": "./function-source.zip",
|
||||||
"output_sha": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
|
"output_sha": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
|
||||||
"output_sha256": "d3001f0d5eed1f8d6312ca50dcb04bbc8127ad0e97536684b4ad865ac31dc820",
|
"output_sha256": "717c7db02d6421b4c31bb065280b5fdc551ab072d9ff064fcd5a3206fb7ca7d4",
|
||||||
"output_sha512": "825b0d0221f34933b2f66183724912c83261055143b4b87c5ebebe852c6d093f27a24f6a3463beeb589344b538d8e3f168f4bf06b6c301725e4fce85deb89e7c",
|
"output_sha512": "75c9ed459e07cf875f0418fb615b13cf1f92100a825d9083f1300087c72be716b7b93d8bb26b5ff33a71a7240478c96c08d685f1d52884643b043f4b2588a03f",
|
||||||
"output_size": 4487,
|
"output_size": 6734,
|
||||||
"source": [],
|
"source": [],
|
||||||
"source_content": null,
|
"source_content": null,
|
||||||
"source_content_filename": null,
|
"source_content_filename": null,
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"effective_annotations": {
|
"effective_annotations": {
|
||||||
"run.googleapis.com/ingress": "all",
|
"run.googleapis.com/ingress": "all",
|
||||||
"run.googleapis.com/ingress-status": "all",
|
"run.googleapis.com/ingress-status": "all",
|
||||||
"run.googleapis.com/operation-id": "7869f742-fe94-42d0-8d82-a1462681980d",
|
"run.googleapis.com/operation-id": "23c7f059-a4af-4cc2-95a4-f0fe54d0d7e6",
|
||||||
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
||||||
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
||||||
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
||||||
@ -181,10 +181,10 @@
|
|||||||
"cloud.googleapis.com/location": "us-central1",
|
"cloud.googleapis.com/location": "us-central1",
|
||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"generation": 1,
|
"generation": 2,
|
||||||
"labels": null,
|
"labels": {},
|
||||||
"namespace": "gen-lang-client-0424120530",
|
"namespace": "gen-lang-client-0424120530",
|
||||||
"resource_version": "AAY16UbSm4k",
|
"resource_version": "AAY18rUlZh4",
|
||||||
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
||||||
"terraform_labels": {
|
"terraform_labels": {
|
||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
@ -216,14 +216,14 @@
|
|||||||
"type": "RoutesReady"
|
"type": "RoutesReady"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"latest_created_revision_name": "sereact-00001-9rv",
|
"latest_created_revision_name": "sereact-00002-q2g",
|
||||||
"latest_ready_revision_name": "sereact-00001-9rv",
|
"latest_ready_revision_name": "sereact-00002-q2g",
|
||||||
"observed_generation": 1,
|
"observed_generation": 2,
|
||||||
"traffic": [
|
"traffic": [
|
||||||
{
|
{
|
||||||
"latest_revision": true,
|
"latest_revision": true,
|
||||||
"percent": 100,
|
"percent": 100,
|
||||||
"revision_name": "sereact-00001-9rv",
|
"revision_name": "sereact-00002-q2g",
|
||||||
"tag": "",
|
"tag": "",
|
||||||
"url": ""
|
"url": ""
|
||||||
}
|
}
|
||||||
@ -256,9 +256,14 @@
|
|||||||
"container_concurrency": 80,
|
"container_concurrency": 80,
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"args": null,
|
"args": [],
|
||||||
"command": null,
|
"command": [],
|
||||||
"env": [
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "API_KEY_SECRET",
|
||||||
|
"value": "super-secret-key-for-development-only",
|
||||||
|
"value_from": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "FIRESTORE_DATABASE_NAME",
|
"name": "FIRESTORE_DATABASE_NAME",
|
||||||
"value": "sereact-imagedb",
|
"value": "sereact-imagedb",
|
||||||
@ -332,7 +337,7 @@
|
|||||||
"cpu": "1",
|
"cpu": "1",
|
||||||
"memory": "1Gi"
|
"memory": "1Gi"
|
||||||
},
|
},
|
||||||
"requests": null
|
"requests": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"startup_probe": [
|
"startup_probe": [
|
||||||
@ -354,7 +359,7 @@
|
|||||||
"working_dir": ""
|
"working_dir": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_selector": null,
|
"node_selector": {},
|
||||||
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"serving_state": "",
|
"serving_state": "",
|
||||||
"timeout_seconds": 300,
|
"timeout_seconds": 300,
|
||||||
@ -469,7 +474,7 @@
|
|||||||
"automatic_update_policy": [
|
"automatic_update_policy": [
|
||||||
{}
|
{}
|
||||||
],
|
],
|
||||||
"build": "projects/761163285547/locations/us-central1/builds/b2b7e513-e00e-462a-8ac8-94abdfb4a0b9",
|
"build": "projects/761163285547/locations/us-central1/builds/47fecac1-7233-42af-ad0e-2a5a9c66e282",
|
||||||
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
||||||
"entry_point": "process_image_embedding",
|
"entry_point": "process_image_embedding",
|
||||||
"environment_variables": {},
|
"environment_variables": {},
|
||||||
@ -483,7 +488,7 @@
|
|||||||
{
|
{
|
||||||
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
||||||
"generation": 1748123369545880,
|
"generation": 1748123369545880,
|
||||||
"object": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip"
|
"object": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -523,14 +528,16 @@
|
|||||||
"FIRESTORE_DATABASE_NAME": "sereact-imagedb",
|
"FIRESTORE_DATABASE_NAME": "sereact-imagedb",
|
||||||
"FIRESTORE_PROJECT_ID": "gen-lang-client-0424120530",
|
"FIRESTORE_PROJECT_ID": "gen-lang-client-0424120530",
|
||||||
"GCS_BUCKET_NAME": "sereact-images",
|
"GCS_BUCKET_NAME": "sereact-images",
|
||||||
|
"GOOGLE_CLOUD_PROJECT": "gen-lang-client-0424120530",
|
||||||
"LOG_EXECUTION_ID": "true",
|
"LOG_EXECUTION_ID": "true",
|
||||||
"LOG_LEVEL": "INFO",
|
"LOG_LEVEL": "INFO",
|
||||||
|
"PROJECT_ID": "gen-lang-client-0424120530",
|
||||||
"QDRANT_API_KEY": "",
|
"QDRANT_API_KEY": "",
|
||||||
"QDRANT_COLLECTION": "image_vectors",
|
"QDRANT_COLLECTION": "image_vectors",
|
||||||
"QDRANT_HOST": "34.71.6.1",
|
"QDRANT_HOST": "34.71.6.1",
|
||||||
"QDRANT_HTTPS": "false",
|
"QDRANT_HTTPS": "false",
|
||||||
"QDRANT_PORT": "6333",
|
"QDRANT_PORT": "6333",
|
||||||
"VISION_API_ENABLED": "true"
|
"VERTEX_AI_LOCATION": "us-central1"
|
||||||
},
|
},
|
||||||
"gcf_uri": "",
|
"gcf_uri": "",
|
||||||
"ingress_settings": "ALLOW_ALL",
|
"ingress_settings": "ALLOW_ALL",
|
||||||
@ -552,7 +559,7 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"timeouts": null,
|
"timeouts": null,
|
||||||
"update_time": "2025-05-24T22:31:52.525335119Z",
|
"update_time": "2025-05-25T09:26:49.473142736Z",
|
||||||
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
@ -868,8 +875,8 @@
|
|||||||
"database_edition": "STANDARD",
|
"database_edition": "STANDARD",
|
||||||
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
||||||
"deletion_policy": "ABANDON",
|
"deletion_policy": "ABANDON",
|
||||||
"earliest_version_time": "2025-05-24T21:29:52.924798Z",
|
"earliest_version_time": "2025-05-25T08:56:42.972923Z",
|
||||||
"etag": "IPHgo4eUvY0DMKrW4vCEvY0D",
|
"etag": "IMzA4cmtvo0DMKrW4vCEvY0D",
|
||||||
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
||||||
"key_prefix": "",
|
"key_prefix": "",
|
||||||
"location_id": "us-central1",
|
"location_id": "us-central1",
|
||||||
@ -900,7 +907,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -924,7 +931,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -948,7 +955,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -972,7 +979,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
@ -989,18 +996,18 @@
|
|||||||
{
|
{
|
||||||
"mode": "managed",
|
"mode": "managed",
|
||||||
"type": "google_project_iam_member",
|
"type": "google_project_iam_member",
|
||||||
"name": "function_vision",
|
"name": "function_vertex_ai",
|
||||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY16LCINIE=",
|
"etag": "BwY16WAsDU4=",
|
||||||
"id": "gen-lang-client-0424120530/roles/ml.developer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"id": "gen-lang-client-0424120530/roles/aiplatform.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
|
||||||
"project": "gen-lang-client-0424120530",
|
"project": "gen-lang-client-0424120530",
|
||||||
"role": "roles/ml.developer"
|
"role": "roles/aiplatform.user"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [],
|
"sensitive_attributes": [],
|
||||||
"private": "bnVsbA==",
|
"private": "bnVsbA==",
|
||||||
@ -1016,6 +1023,20 @@
|
|||||||
"name": "services",
|
"name": "services",
|
||||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||||
"instances": [
|
"instances": [
|
||||||
|
{
|
||||||
|
"index_key": "aiplatform.googleapis.com",
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"disable_dependent_services": null,
|
||||||
|
"disable_on_destroy": false,
|
||||||
|
"id": "gen-lang-client-0424120530/aiplatform.googleapis.com",
|
||||||
|
"project": "gen-lang-client-0424120530",
|
||||||
|
"service": "aiplatform.googleapis.com",
|
||||||
|
"timeouts": null
|
||||||
|
},
|
||||||
|
"sensitive_attributes": [],
|
||||||
|
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"index_key": "cloudbuild.googleapis.com",
|
"index_key": "cloudbuild.googleapis.com",
|
||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
@ -1493,21 +1514,21 @@
|
|||||||
"content_encoding": "",
|
"content_encoding": "",
|
||||||
"content_language": "",
|
"content_language": "",
|
||||||
"content_type": "application/zip",
|
"content_type": "application/zip",
|
||||||
"crc32c": "Y4Q5hw==",
|
"crc32c": "kTROsA==",
|
||||||
"customer_encryption": [],
|
"customer_encryption": [],
|
||||||
"detect_md5hash": "pdOn/hMclyv40O3zCVRQQg==",
|
"detect_md5hash": "Ru+hruU4bi8kS1lyicfEug==",
|
||||||
"event_based_hold": false,
|
"event_based_hold": false,
|
||||||
"generation": 1748125796837241,
|
"generation": 1748126317190781,
|
||||||
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
|
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"kms_key_name": "",
|
"kms_key_name": "",
|
||||||
"md5hash": "pdOn/hMclyv40O3zCVRQQg==",
|
"md5hash": "Ru+hruU4bi8kS1lyicfEug==",
|
||||||
"md5hexhash": "a5d3a7fe131c972bf8d0edf309545042",
|
"md5hexhash": "46efa1aee5386e2f244b597289c7c4ba",
|
||||||
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip?generation=1748125796837241\u0026alt=media",
|
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip?generation=1748126317190781\u0026alt=media",
|
||||||
"metadata": null,
|
"metadata": {},
|
||||||
"name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
|
"name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"output_name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
|
"output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"retention": [],
|
"retention": [],
|
||||||
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
|
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
|
||||||
"source": "./function-source.zip",
|
"source": "./function-source.zip",
|
||||||
"storage_class": "STANDARD",
|
"storage_class": "STANDARD",
|
||||||
"temporary_hold": false,
|
"temporary_hold": false,
|
||||||
|
|||||||
BIN
deployment/terraform/test_image.jpg
Normal file
BIN
deployment/terraform/test_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to set up Firestore credentials for development and deployment
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def create_env_file(project_id, credentials_file="firestore-credentials.json"):
|
|
||||||
"""Create a .env file with the necessary environment variables"""
|
|
||||||
env_content = f"""# Firestore Settings
|
|
||||||
FIRESTORE_PROJECT_ID={project_id}
|
|
||||||
FIRESTORE_CREDENTIALS_FILE={credentials_file}
|
|
||||||
|
|
||||||
# Google Cloud Storage Settings
|
|
||||||
GCS_BUCKET_NAME={project_id}-storage
|
|
||||||
GCS_CREDENTIALS_FILE={credentials_file}
|
|
||||||
|
|
||||||
# Security settings
|
|
||||||
API_KEY_SECRET=development-secret-key-change-in-production
|
|
||||||
API_KEY_EXPIRY_DAYS=365
|
|
||||||
|
|
||||||
# Vector Database Settings
|
|
||||||
VECTOR_DB_API_KEY=
|
|
||||||
VECTOR_DB_ENVIRONMENT=
|
|
||||||
VECTOR_DB_INDEX_NAME=image-embeddings
|
|
||||||
|
|
||||||
# Other Settings
|
|
||||||
ENVIRONMENT=development
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
RATE_LIMIT_PER_MINUTE=100
|
|
||||||
"""
|
|
||||||
|
|
||||||
with open(".env", "w") as f:
|
|
||||||
f.write(env_content)
|
|
||||||
|
|
||||||
print("Created .env file with Firestore settings")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description='Set up Firestore credentials')
|
|
||||||
parser.add_argument('--key-file', type=str, help='Path to the service account key file')
|
|
||||||
parser.add_argument('--project-id', type=str, help='Google Cloud project ID')
|
|
||||||
parser.add_argument('--create-env', action='store_true', help='Create .env file')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Ensure we have a project ID
|
|
||||||
project_id = args.project_id
|
|
||||||
if not project_id:
|
|
||||||
if args.key_file and os.path.exists(args.key_file):
|
|
||||||
try:
|
|
||||||
with open(args.key_file, 'r') as f:
|
|
||||||
key_data = json.load(f)
|
|
||||||
project_id = key_data.get('project_id')
|
|
||||||
if project_id:
|
|
||||||
print(f"Using project ID from key file: {project_id}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading key file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not project_id:
|
|
||||||
print("Error: Project ID is required")
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Handle key file
|
|
||||||
target_key_file = "firestore-credentials.json"
|
|
||||||
if args.key_file and os.path.exists(args.key_file):
|
|
||||||
# Copy the key file to the target location
|
|
||||||
try:
|
|
||||||
with open(args.key_file, 'r') as src, open(target_key_file, 'w') as dst:
|
|
||||||
key_data = json.load(src)
|
|
||||||
json.dump(key_data, dst, indent=2)
|
|
||||||
print(f"Copied service account key to {target_key_file}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error copying key file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("Warning: No service account key file provided")
|
|
||||||
print(f"You need to place your service account key in {target_key_file}")
|
|
||||||
|
|
||||||
# Create .env file if requested
|
|
||||||
if args.create_env:
|
|
||||||
create_env_file(project_id, target_key_file)
|
|
||||||
|
|
||||||
print("\nSetup complete!")
|
|
||||||
print("\nFor development:")
|
|
||||||
print(f"1. Make sure {target_key_file} exists in the project root")
|
|
||||||
print("2. Ensure environment variables are set in .env file")
|
|
||||||
print("\nFor deployment:")
|
|
||||||
print("1. For Cloud Run, set environment variables in deployment config")
|
|
||||||
print("2. Make sure to securely manage service account key")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -25,6 +25,13 @@ storage_service = StorageService()
|
|||||||
image_processor = ImageProcessor()
|
image_processor = ImageProcessor()
|
||||||
embedding_service = EmbeddingService()
|
embedding_service = EmbeddingService()
|
||||||
|
|
||||||
|
def generate_api_download_url(request: Request, image_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate API download URL for an image
|
||||||
|
"""
|
||||||
|
base_url = str(request.base_url).rstrip('/')
|
||||||
|
return f"{base_url}/api/v1/images/{image_id}/download"
|
||||||
|
|
||||||
@router.post("", response_model=ImageResponse, status_code=201)
|
@router.post("", response_model=ImageResponse, status_code=201)
|
||||||
async def upload_image(
|
async def upload_image(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -62,9 +69,6 @@ async def upload_image(
|
|||||||
file, str(current_user.team_id)
|
file, str(current_user.team_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate public URL
|
|
||||||
public_url = storage_service.generate_public_url(storage_path)
|
|
||||||
|
|
||||||
# Process tags
|
# Process tags
|
||||||
tag_list = []
|
tag_list = []
|
||||||
if tags:
|
if tags:
|
||||||
@ -77,7 +81,7 @@ async def upload_image(
|
|||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
storage_path=storage_path,
|
storage_path=storage_path,
|
||||||
public_url=public_url,
|
public_url="", # Will be set after we have the image ID
|
||||||
team_id=current_user.team_id,
|
team_id=current_user.team_id,
|
||||||
uploader_id=current_user.id,
|
uploader_id=current_user.id,
|
||||||
description=description,
|
description=description,
|
||||||
@ -89,6 +93,13 @@ async def upload_image(
|
|||||||
# Save to database
|
# Save to database
|
||||||
created_image = await image_repository.create(image)
|
created_image = await image_repository.create(image)
|
||||||
|
|
||||||
|
# Generate API download URL now that we have the image ID
|
||||||
|
api_download_url = generate_api_download_url(request, str(created_image.id))
|
||||||
|
|
||||||
|
# Update the image with the API download URL
|
||||||
|
await image_repository.update(created_image.id, {"public_url": api_download_url})
|
||||||
|
created_image.public_url = api_download_url
|
||||||
|
|
||||||
# Publish image processing task to Pub/Sub
|
# Publish image processing task to Pub/Sub
|
||||||
try:
|
try:
|
||||||
task_published = await pubsub_service.publish_image_processing_task(
|
task_published = await pubsub_service.publish_image_processing_task(
|
||||||
@ -196,18 +207,8 @@ async def list_images(
|
|||||||
# Convert to response
|
# Convert to response
|
||||||
response_images = []
|
response_images = []
|
||||||
for image in images:
|
for image in images:
|
||||||
# Generate public URL if not set
|
# Generate API download URL
|
||||||
public_url = image.public_url
|
api_download_url = generate_api_download_url(request, str(image.id))
|
||||||
if not public_url and image.storage_path:
|
|
||||||
# Make the file public and generate URL
|
|
||||||
try:
|
|
||||||
storage_service.make_file_public(image.storage_path)
|
|
||||||
public_url = storage_service.generate_public_url(image.storage_path)
|
|
||||||
# Update the database with the public URL
|
|
||||||
await image_repository.update(image.id, {"public_url": public_url})
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to make file public or update URL for image {image.id}: {e}")
|
|
||||||
public_url = storage_service.generate_public_url(image.storage_path)
|
|
||||||
|
|
||||||
response_images.append(ImageResponse(
|
response_images.append(ImageResponse(
|
||||||
id=str(image.id),
|
id=str(image.id),
|
||||||
@ -216,7 +217,7 @@ async def list_images(
|
|||||||
file_size=image.file_size,
|
file_size=image.file_size,
|
||||||
content_type=image.content_type,
|
content_type=image.content_type,
|
||||||
storage_path=image.storage_path,
|
storage_path=image.storage_path,
|
||||||
public_url=public_url,
|
public_url=api_download_url,
|
||||||
team_id=str(image.team_id),
|
team_id=str(image.team_id),
|
||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
@ -258,21 +259,8 @@ async def get_image(
|
|||||||
if not current_user.is_admin and image.team_id != current_user.team_id:
|
if not current_user.is_admin and image.team_id != current_user.team_id:
|
||||||
raise HTTPException(status_code=403, detail="Not authorized to access this image")
|
raise HTTPException(status_code=403, detail="Not authorized to access this image")
|
||||||
|
|
||||||
# Update last accessed
|
# Generate API download URL
|
||||||
await image_repository.update_last_accessed(obj_id)
|
api_download_url = generate_api_download_url(request, str(image.id))
|
||||||
|
|
||||||
# Generate public URL if not set
|
|
||||||
public_url = image.public_url
|
|
||||||
if not public_url and image.storage_path:
|
|
||||||
# Make the file public and generate URL
|
|
||||||
try:
|
|
||||||
storage_service.make_file_public(image.storage_path)
|
|
||||||
public_url = storage_service.generate_public_url(image.storage_path)
|
|
||||||
# Update the database with the public URL
|
|
||||||
await image_repository.update(image.id, {"public_url": public_url})
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to make file public or update URL for image {image.id}: {e}")
|
|
||||||
public_url = storage_service.generate_public_url(image.storage_path)
|
|
||||||
|
|
||||||
# Convert to response
|
# Convert to response
|
||||||
response = ImageResponse(
|
response = ImageResponse(
|
||||||
@ -282,7 +270,7 @@ async def get_image(
|
|||||||
file_size=image.file_size,
|
file_size=image.file_size,
|
||||||
content_type=image.content_type,
|
content_type=image.content_type,
|
||||||
storage_path=image.storage_path,
|
storage_path=image.storage_path,
|
||||||
public_url=public_url,
|
public_url=api_download_url,
|
||||||
team_id=str(image.team_id),
|
team_id=str(image.team_id),
|
||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
@ -374,6 +362,7 @@ async def update_image(
|
|||||||
update_data = image_data.dict(exclude_unset=True)
|
update_data = image_data.dict(exclude_unset=True)
|
||||||
if not update_data:
|
if not update_data:
|
||||||
# No fields to update
|
# No fields to update
|
||||||
|
api_download_url = generate_api_download_url(request, str(image.id))
|
||||||
response = ImageResponse(
|
response = ImageResponse(
|
||||||
id=str(image.id),
|
id=str(image.id),
|
||||||
filename=image.filename,
|
filename=image.filename,
|
||||||
@ -381,6 +370,7 @@ async def update_image(
|
|||||||
file_size=image.file_size,
|
file_size=image.file_size,
|
||||||
content_type=image.content_type,
|
content_type=image.content_type,
|
||||||
storage_path=image.storage_path,
|
storage_path=image.storage_path,
|
||||||
|
public_url=api_download_url,
|
||||||
team_id=str(image.team_id),
|
team_id=str(image.team_id),
|
||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
@ -396,6 +386,9 @@ async def update_image(
|
|||||||
if not updated_image:
|
if not updated_image:
|
||||||
raise HTTPException(status_code=500, detail="Failed to update image")
|
raise HTTPException(status_code=500, detail="Failed to update image")
|
||||||
|
|
||||||
|
# Generate API download URL
|
||||||
|
api_download_url = generate_api_download_url(request, str(updated_image.id))
|
||||||
|
|
||||||
# Convert to response
|
# Convert to response
|
||||||
response = ImageResponse(
|
response = ImageResponse(
|
||||||
id=str(updated_image.id),
|
id=str(updated_image.id),
|
||||||
@ -404,6 +397,7 @@ async def update_image(
|
|||||||
file_size=updated_image.file_size,
|
file_size=updated_image.file_size,
|
||||||
content_type=updated_image.content_type,
|
content_type=updated_image.content_type,
|
||||||
storage_path=updated_image.storage_path,
|
storage_path=updated_image.storage_path,
|
||||||
|
public_url=api_download_url,
|
||||||
team_id=str(updated_image.team_id),
|
team_id=str(updated_image.team_id),
|
||||||
uploader_id=str(updated_image.uploader_id),
|
uploader_id=str(updated_image.uploader_id),
|
||||||
upload_date=updated_image.upload_date,
|
upload_date=updated_image.upload_date,
|
||||||
|
|||||||
118
tests/test_admin_api.py
Normal file
118
tests/test_admin_api.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify admin image access functionality via API endpoints.
|
||||||
|
This script tests the actual HTTP endpoints to ensure admin users can see all images.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
API_BASE = f"{BASE_URL}/api/v1"
|
||||||
|
|
||||||
|
def test_api_endpoints():
|
||||||
|
"""Test the admin functionality via API endpoints"""
|
||||||
|
print("🧪 Testing Admin Image Access via API")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# You'll need to replace these with actual API keys from your system
|
||||||
|
# Get these by running: python scripts/get_test_api_key.py
|
||||||
|
print("📝 To run this test, you need:")
|
||||||
|
print("1. A running API server (python main.py)")
|
||||||
|
print("2. Valid API keys for both admin and regular users")
|
||||||
|
print("3. Update the API_KEYS section below with real keys")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Example API keys (replace with real ones)
|
||||||
|
API_KEYS = {
|
||||||
|
"admin": "your_admin_api_key_here",
|
||||||
|
"regular": "your_regular_api_key_here"
|
||||||
|
}
|
||||||
|
|
||||||
|
if API_KEYS["admin"] == "your_admin_api_key_here":
|
||||||
|
print("❌ Please update the API_KEYS in this script with real API keys")
|
||||||
|
print(" Run: python scripts/get_test_api_key.py")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test regular user access
|
||||||
|
print("\n=== Testing Regular User API Access ===")
|
||||||
|
headers_regular = {"X-API-Key": API_KEYS["regular"]}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE}/images", headers=headers_regular)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
regular_count = data.get("total", 0)
|
||||||
|
print(f"Regular user sees {regular_count} images")
|
||||||
|
print(f"Images returned: {len(data.get('images', []))}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Regular user API call failed: {response.status_code}")
|
||||||
|
print(response.text)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error calling regular user API: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test admin user access
|
||||||
|
print("\n=== Testing Admin User API Access ===")
|
||||||
|
headers_admin = {"X-API-Key": API_KEYS["admin"]}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE}/images", headers=headers_admin)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
admin_count = data.get("total", 0)
|
||||||
|
print(f"Admin user sees {admin_count} images")
|
||||||
|
print(f"Images returned: {len(data.get('images', []))}")
|
||||||
|
|
||||||
|
# Show teams represented in the results
|
||||||
|
teams = set()
|
||||||
|
for image in data.get('images', []):
|
||||||
|
teams.add(image.get('team_id'))
|
||||||
|
print(f"Images from {len(teams)} different teams")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"❌ Admin user API call failed: {response.status_code}")
|
||||||
|
print(response.text)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error calling admin user API: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compare results
|
||||||
|
print(f"\n=== Summary ===")
|
||||||
|
print(f"Regular user images: {regular_count}")
|
||||||
|
print(f"Admin user images: {admin_count}")
|
||||||
|
|
||||||
|
if admin_count >= regular_count:
|
||||||
|
print("✅ SUCCESS: Admin sees same or more images than regular user")
|
||||||
|
if admin_count > regular_count:
|
||||||
|
print("✅ PERFECT: Admin sees more images (cross-team access working)")
|
||||||
|
else:
|
||||||
|
print("ℹ️ NOTE: Admin and regular user see same count (might be same team or no other teams)")
|
||||||
|
else:
|
||||||
|
print("❌ FAILURE: Admin should see at least as many images as regular user")
|
||||||
|
|
||||||
|
def get_sample_api_keys():
|
||||||
|
"""Helper function to show how to get API keys"""
|
||||||
|
print("\n📋 How to get API keys for testing:")
|
||||||
|
print("1. Make sure your API server is running:")
|
||||||
|
print(" python main.py")
|
||||||
|
print()
|
||||||
|
print("2. Get a regular user API key:")
|
||||||
|
print(" python scripts/get_test_api_key.py")
|
||||||
|
print()
|
||||||
|
print("3. Get an admin user API key:")
|
||||||
|
print(" python scripts/create_admin.py")
|
||||||
|
print(" # Then use the admin email to get their API key")
|
||||||
|
print()
|
||||||
|
print("4. Update the API_KEYS dictionary in this script")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--help":
|
||||||
|
get_sample_api_keys()
|
||||||
|
else:
|
||||||
|
test_api_endpoints()
|
||||||
Loading…
x
Reference in New Issue
Block a user