-

+
+

+
+
${Math.round(result.similarity * 100)}% match
@@ -134,6 +136,36 @@ function displaySearchResults(results, query) {
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
addSearchRefinementOptions(query, results);
}
diff --git a/client/js/ui.js b/client/js/ui.js
index 1521ed3..25dbcc8 100644
--- a/client/js/ui.js
+++ b/client/js/ui.js
@@ -5,6 +5,12 @@ function showPage(pageId) {
try {
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
const pages = document.querySelectorAll('.page');
console.log('Found pages:', pages.length);
@@ -40,6 +46,7 @@ function showPage(pageId) {
if (window.SeReactApp) {
window.SeReactApp.currentPage = pageId;
}
+ app.currentPage = pageId; // Also update the local app state
// Load page data if needed
console.log('About to load page data for:', pageId);
diff --git a/client/styles.css b/client/styles.css
index f05fc92..01b3383 100644
--- a/client/styles.css
+++ b/client/styles.css
@@ -99,6 +99,15 @@ body {
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 {
to { transform: rotate(360deg); }
}
diff --git a/debug_api_key_deployment.py b/debug_api_key_deployment.py
new file mode 100644
index 0000000..183c3ca
--- /dev/null
+++ b/debug_api_key_deployment.py
@@ -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()
\ No newline at end of file
diff --git a/deployment/terraform/main.tf b/deployment/terraform/main.tf
index 04f5340..210af6e 100644
--- a/deployment/terraform/main.tf
+++ b/deployment/terraform/main.tf
@@ -144,6 +144,11 @@ resource "google_cloud_run_service" "sereact" {
name = "LOG_LEVEL"
value = "INFO"
}
+
+ env {
+ name = "API_KEY_SECRET"
+ value = "super-secret-key-for-development-only"
+ }
}
}
}
diff --git a/deployment/terraform/terraform.tfstate b/deployment/terraform/terraform.tfstate
index d05a72e..fb755f2 100644
--- a/deployment/terraform/terraform.tfstate
+++ b/deployment/terraform/terraform.tfstate
@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.10.1",
- "serial": 417,
+ "serial": 425,
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
"outputs": {
"cloud_function_name": {
@@ -172,7 +172,7 @@
"effective_annotations": {
"run.googleapis.com/ingress": "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\"]",
"serving.knative.dev/creator": "johnpccd3@gmail.com",
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
@@ -182,14 +182,14 @@
"goog-terraform-provisioned": "true"
},
"generation": 1,
- "labels": {},
+ "labels": null,
"namespace": "gen-lang-client-0424120530",
- "resource_version": "AAY16UbSm4k",
+ "resource_version": "AAY18uNZfE8",
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
"terraform_labels": {
"goog-terraform-provisioned": "true"
},
- "uid": "d5532269-ab10-4b77-b90f-698306bf0919"
+ "uid": "dfae23a0-4b71-40aa-baa8-64f5b842501a"
}
],
"name": "sereact",
@@ -216,14 +216,14 @@
"type": "RoutesReady"
}
],
- "latest_created_revision_name": "sereact-00001-9rv",
- "latest_ready_revision_name": "sereact-00001-9rv",
+ "latest_created_revision_name": "sereact-00001-58h",
+ "latest_ready_revision_name": "sereact-00001-58h",
"observed_generation": 1,
"traffic": [
{
"latest_revision": true,
"percent": 100,
- "revision_name": "sereact-00001-9rv",
+ "revision_name": "sereact-00001-58h",
"tag": "",
"url": ""
}
@@ -256,9 +256,14 @@
"container_concurrency": 80,
"containers": [
{
- "args": [],
- "command": [],
+ "args": null,
+ "command": null,
"env": [
+ {
+ "name": "API_KEY_SECRET",
+ "value": "super-secret-key-for-development-only",
+ "value_from": []
+ },
{
"name": "FIRESTORE_DATABASE_NAME",
"value": "sereact-imagedb",
@@ -332,7 +337,7 @@
"cpu": "1",
"memory": "1Gi"
},
- "requests": {}
+ "requests": null
}
],
"startup_probe": [
@@ -354,7 +359,7 @@
"working_dir": ""
}
],
- "node_selector": {},
+ "node_selector": null,
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
"serving_state": "",
"timeout_seconds": 300,
@@ -435,7 +440,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16UdHJ00=",
+ "etag": "BwY18uO4z7A=",
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
"location": "us-central1",
"member": "allUsers",
@@ -469,7 +474,7 @@
"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",
"entry_point": "process_image_embedding",
"environment_variables": {},
@@ -526,6 +531,7 @@
"GOOGLE_CLOUD_PROJECT": "gen-lang-client-0424120530",
"LOG_EXECUTION_ID": "true",
"LOG_LEVEL": "INFO",
+ "PROJECT_ID": "gen-lang-client-0424120530",
"QDRANT_API_KEY": "",
"QDRANT_COLLECTION": "image_vectors",
"QDRANT_HOST": "34.71.6.1",
@@ -553,7 +559,7 @@
"goog-terraform-provisioned": "true"
},
"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"
},
"sensitive_attributes": [
@@ -803,12 +809,6 @@
"zone": "us-central1-a"
},
"sensitive_attributes": [
- [
- {
- "type": "get_attr",
- "value": "metadata_startup_script"
- }
- ],
[
{
"type": "get_attr",
@@ -842,6 +842,12 @@
"type": "get_attr",
"value": "disk_encryption_key_rsa"
}
+ ],
+ [
+ {
+ "type": "get_attr",
+ "value": "metadata_startup_script"
+ }
]
],
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiNiJ9",
@@ -869,8 +875,8 @@
"database_edition": "STANDARD",
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"deletion_policy": "ABANDON",
- "earliest_version_time": "2025-05-24T21:38:29.341046Z",
- "etag": "IOGow/2VvY0DMKrW4vCEvY0D",
+ "earliest_version_time": "2025-05-25T08:59:14.308781Z",
+ "etag": "IMCn9pGuvo0DMKrW4vCEvY0D",
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
"key_prefix": "",
"location_id": "us-central1",
@@ -901,7 +907,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -925,7 +931,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -949,7 +955,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -973,7 +979,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -1518,7 +1524,7 @@
"md5hash": "Ru+hruU4bi8kS1lyicfEug==",
"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",
- "metadata": null,
+ "metadata": {},
"name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"retention": [],
diff --git a/deployment/terraform/terraform.tfstate.backup b/deployment/terraform/terraform.tfstate.backup
index 3a277b6..8e9163a 100644
--- a/deployment/terraform/terraform.tfstate.backup
+++ b/deployment/terraform/terraform.tfstate.backup
@@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.10.1",
- "serial": 410,
+ "serial": 422,
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
"outputs": {
"cloud_function_name": {
@@ -98,16 +98,16 @@
"attributes": {
"exclude_symlink_directories": null,
"excludes": null,
- "id": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
- "output_base64sha256": "0wAfDV7tH41jEspQ3LBLvIEnrQ6XU2aEtK2GWsMdyCA=",
- "output_base64sha512": "glsNAiHzSTOy9mGDckkSyDJhBVFDtLh8Xr6+hSxtCT8nok9qNGO+61iTRLU42OPxaPS/BrbDAXJeT86F3riefA==",
+ "id": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
+ "output_base64sha256": "cXx9sC1kIbTDG7BlKAtf3FUasHLZ/wZPzVoyBvt8p9Q=",
+ "output_base64sha512": "dcntRZ4Hz4dfBBj7YVsTzx+SEAqCXZCD8TAAh8cr5xa3uT2Lsmtf8zpxpyQEeMlsCNaF8dUohGQ7BD9LJYigPw==",
"output_file_mode": null,
- "output_md5": "a5d3a7fe131c972bf8d0edf309545042",
+ "output_md5": "46efa1aee5386e2f244b597289c7c4ba",
"output_path": "./function-source.zip",
- "output_sha": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
- "output_sha256": "d3001f0d5eed1f8d6312ca50dcb04bbc8127ad0e97536684b4ad865ac31dc820",
- "output_sha512": "825b0d0221f34933b2f66183724912c83261055143b4b87c5ebebe852c6d093f27a24f6a3463beeb589344b538d8e3f168f4bf06b6c301725e4fce85deb89e7c",
- "output_size": 4487,
+ "output_sha": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
+ "output_sha256": "717c7db02d6421b4c31bb065280b5fdc551ab072d9ff064fcd5a3206fb7ca7d4",
+ "output_sha512": "75c9ed459e07cf875f0418fb615b13cf1f92100a825d9083f1300087c72be716b7b93d8bb26b5ff33a71a7240478c96c08d685f1d52884643b043f4b2588a03f",
+ "output_size": 6734,
"source": [],
"source_content": null,
"source_content_filename": null,
@@ -172,7 +172,7 @@
"effective_annotations": {
"run.googleapis.com/ingress": "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\"]",
"serving.knative.dev/creator": "johnpccd3@gmail.com",
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
@@ -181,10 +181,10 @@
"cloud.googleapis.com/location": "us-central1",
"goog-terraform-provisioned": "true"
},
- "generation": 1,
- "labels": null,
+ "generation": 2,
+ "labels": {},
"namespace": "gen-lang-client-0424120530",
- "resource_version": "AAY16UbSm4k",
+ "resource_version": "AAY18rUlZh4",
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
"terraform_labels": {
"goog-terraform-provisioned": "true"
@@ -216,14 +216,14 @@
"type": "RoutesReady"
}
],
- "latest_created_revision_name": "sereact-00001-9rv",
- "latest_ready_revision_name": "sereact-00001-9rv",
- "observed_generation": 1,
+ "latest_created_revision_name": "sereact-00002-q2g",
+ "latest_ready_revision_name": "sereact-00002-q2g",
+ "observed_generation": 2,
"traffic": [
{
"latest_revision": true,
"percent": 100,
- "revision_name": "sereact-00001-9rv",
+ "revision_name": "sereact-00002-q2g",
"tag": "",
"url": ""
}
@@ -256,9 +256,14 @@
"container_concurrency": 80,
"containers": [
{
- "args": null,
- "command": null,
+ "args": [],
+ "command": [],
"env": [
+ {
+ "name": "API_KEY_SECRET",
+ "value": "super-secret-key-for-development-only",
+ "value_from": []
+ },
{
"name": "FIRESTORE_DATABASE_NAME",
"value": "sereact-imagedb",
@@ -332,7 +337,7 @@
"cpu": "1",
"memory": "1Gi"
},
- "requests": null
+ "requests": {}
}
],
"startup_probe": [
@@ -354,7 +359,7 @@
"working_dir": ""
}
],
- "node_selector": null,
+ "node_selector": {},
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
"serving_state": "",
"timeout_seconds": 300,
@@ -469,7 +474,7 @@
"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",
"entry_point": "process_image_embedding",
"environment_variables": {},
@@ -483,7 +488,7 @@
{
"bucket": "gen-lang-client-0424120530-cloud-function-source",
"generation": 1748123369545880,
- "object": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip"
+ "object": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip"
}
]
}
@@ -523,14 +528,16 @@
"FIRESTORE_DATABASE_NAME": "sereact-imagedb",
"FIRESTORE_PROJECT_ID": "gen-lang-client-0424120530",
"GCS_BUCKET_NAME": "sereact-images",
+ "GOOGLE_CLOUD_PROJECT": "gen-lang-client-0424120530",
"LOG_EXECUTION_ID": "true",
"LOG_LEVEL": "INFO",
+ "PROJECT_ID": "gen-lang-client-0424120530",
"QDRANT_API_KEY": "",
"QDRANT_COLLECTION": "image_vectors",
"QDRANT_HOST": "34.71.6.1",
"QDRANT_HTTPS": "false",
"QDRANT_PORT": "6333",
- "VISION_API_ENABLED": "true"
+ "VERTEX_AI_LOCATION": "us-central1"
},
"gcf_uri": "",
"ingress_settings": "ALLOW_ALL",
@@ -552,7 +559,7 @@
"goog-terraform-provisioned": "true"
},
"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"
},
"sensitive_attributes": [
@@ -868,8 +875,8 @@
"database_edition": "STANDARD",
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"deletion_policy": "ABANDON",
- "earliest_version_time": "2025-05-24T21:29:52.924798Z",
- "etag": "IPHgo4eUvY0DMKrW4vCEvY0D",
+ "earliest_version_time": "2025-05-25T08:56:42.972923Z",
+ "etag": "IMzA4cmtvo0DMKrW4vCEvY0D",
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
"key_prefix": "",
"location_id": "us-central1",
@@ -900,7 +907,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -924,7 +931,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -948,7 +955,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -972,7 +979,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
+ "etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
@@ -989,18 +996,18 @@
{
"mode": "managed",
"type": "google_project_iam_member",
- "name": "function_vision",
+ "name": "function_vertex_ai",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"condition": [],
- "etag": "BwY16LCINIE=",
- "id": "gen-lang-client-0424120530/roles/ml.developer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
+ "etag": "BwY16WAsDU4=",
+ "id": "gen-lang-client-0424120530/roles/aiplatform.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
- "role": "roles/ml.developer"
+ "role": "roles/aiplatform.user"
},
"sensitive_attributes": [],
"private": "bnVsbA==",
@@ -1016,6 +1023,20 @@
"name": "services",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"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",
"schema_version": 0,
@@ -1493,21 +1514,21 @@
"content_encoding": "",
"content_language": "",
"content_type": "application/zip",
- "crc32c": "Y4Q5hw==",
+ "crc32c": "kTROsA==",
"customer_encryption": [],
- "detect_md5hash": "pdOn/hMclyv40O3zCVRQQg==",
+ "detect_md5hash": "Ru+hruU4bi8kS1lyicfEug==",
"event_based_hold": false,
- "generation": 1748125796837241,
- "id": "gen-lang-client-0424120530-cloud-function-source-function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
+ "generation": 1748126317190781,
+ "id": "gen-lang-client-0424120530-cloud-function-source-function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"kms_key_name": "",
- "md5hash": "pdOn/hMclyv40O3zCVRQQg==",
- "md5hexhash": "a5d3a7fe131c972bf8d0edf309545042",
- "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",
- "metadata": null,
- "name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
- "output_name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
+ "md5hash": "Ru+hruU4bi8kS1lyicfEug==",
+ "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",
+ "metadata": {},
+ "name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
+ "output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"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",
"storage_class": "STANDARD",
"temporary_hold": false,
diff --git a/deployment/terraform/test_image.jpg b/deployment/terraform/test_image.jpg
new file mode 100644
index 0000000..79b7c1f
Binary files /dev/null and b/deployment/terraform/test_image.jpg differ
diff --git a/scripts/setup_credentials.py b/scripts/setup_credentials.py
deleted file mode 100644
index 5b35fd9..0000000
--- a/scripts/setup_credentials.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/src/api/v1/images.py b/src/api/v1/images.py
index 04761df..4506cbd 100644
--- a/src/api/v1/images.py
+++ b/src/api/v1/images.py
@@ -25,6 +25,13 @@ storage_service = StorageService()
image_processor = ImageProcessor()
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)
async def upload_image(
request: Request,
@@ -62,9 +69,6 @@ async def upload_image(
file, str(current_user.team_id)
)
- # Generate public URL
- public_url = storage_service.generate_public_url(storage_path)
-
# Process tags
tag_list = []
if tags:
@@ -77,7 +81,7 @@ async def upload_image(
file_size=file_size,
content_type=content_type,
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,
uploader_id=current_user.id,
description=description,
@@ -89,6 +93,13 @@ async def upload_image(
# Save to database
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
try:
task_published = await pubsub_service.publish_image_processing_task(
@@ -196,18 +207,8 @@ async def list_images(
# Convert to response
response_images = []
for image in images:
- # 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)
+ # Generate API download URL
+ api_download_url = generate_api_download_url(request, str(image.id))
response_images.append(ImageResponse(
id=str(image.id),
@@ -216,7 +217,7 @@ async def list_images(
file_size=image.file_size,
content_type=image.content_type,
storage_path=image.storage_path,
- public_url=public_url,
+ public_url=api_download_url,
team_id=str(image.team_id),
uploader_id=str(image.uploader_id),
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:
raise HTTPException(status_code=403, detail="Not authorized to access this image")
- # Update last accessed
- await image_repository.update_last_accessed(obj_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)
+ # Generate API download URL
+ api_download_url = generate_api_download_url(request, str(image.id))
# Convert to response
response = ImageResponse(
@@ -282,7 +270,7 @@ async def get_image(
file_size=image.file_size,
content_type=image.content_type,
storage_path=image.storage_path,
- public_url=public_url,
+ public_url=api_download_url,
team_id=str(image.team_id),
uploader_id=str(image.uploader_id),
upload_date=image.upload_date,
@@ -374,6 +362,7 @@ async def update_image(
update_data = image_data.dict(exclude_unset=True)
if not update_data:
# No fields to update
+ api_download_url = generate_api_download_url(request, str(image.id))
response = ImageResponse(
id=str(image.id),
filename=image.filename,
@@ -381,6 +370,7 @@ async def update_image(
file_size=image.file_size,
content_type=image.content_type,
storage_path=image.storage_path,
+ public_url=api_download_url,
team_id=str(image.team_id),
uploader_id=str(image.uploader_id),
upload_date=image.upload_date,
@@ -396,6 +386,9 @@ async def update_image(
if not updated_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
response = ImageResponse(
id=str(updated_image.id),
@@ -404,6 +397,7 @@ async def update_image(
file_size=updated_image.file_size,
content_type=updated_image.content_type,
storage_path=updated_image.storage_path,
+ public_url=api_download_url,
team_id=str(updated_image.team_id),
uploader_id=str(updated_image.uploader_id),
upload_date=updated_image.upload_date,
diff --git a/tests/test_admin_api.py b/tests/test_admin_api.py
new file mode 100644
index 0000000..2d01a29
--- /dev/null
+++ b/tests/test_admin_api.py
@@ -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()
\ No newline at end of file