fix showing images on client

This commit is contained in:
johnpccd 2025-05-25 12:15:18 +02:00
parent cc2810bbf9
commit 273b20a76c
13 changed files with 537 additions and 243 deletions

View File

@ -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 = {

View File

@ -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;">
alt="${escapeHtml(image.description || 'Image')}" <img id="img-${image.id}"
onclick="viewImage('${image.id}')" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InRyYW5zcGFyZW50Ii8+PC9zdmc+"
style="cursor: pointer;"> alt="${escapeHtml(image.description || 'Image')}"
onclick="viewImage('${image.id}')"
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);
} }
}); });

View File

@ -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;">
class="card-img-top" <img id="search-img-${result.image.id}"
alt="${escapeHtml(result.image.description || 'Image')}" src="/placeholder-image.png"
style="height: 200px; object-fit: cover; cursor: pointer;" class="card-img-top"
onclick="viewImage('${result.image.id}')"> alt="${escapeHtml(result.image.description || 'Image')}"
style="height: 200px; object-fit: cover; cursor: pointer; opacity: 0.5;"
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);
} }

View File

@ -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);

View File

@ -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); }
} }

View 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()

View File

@ -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"
}
} }
} }
} }

View File

@ -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": [],

View File

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -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()

View File

@ -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
View 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()