diff --git a/client/js/api.js b/client/js/api.js index 0b3c2a5..01107ce 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -124,7 +124,8 @@ class ApiClient { // Images API async getImages(page = 1, limit = 20, tags = null) { - let endpoint = `/images?page=${page}&limit=${limit}`; + const skip = (page - 1) * limit; + let endpoint = `/images?skip=${skip}&limit=${limit}`; if (tags) { endpoint += `&tags=${encodeURIComponent(tags)}`; } @@ -173,18 +174,6 @@ class ApiClient { const response = await fetch(`${this.baseUrl}/health`); return response.ok; } - - // Get image URL for display - getImageUrl(imageId) { - this.updateConfig(); - return `${this.baseUrl}/api/v1/images/${imageId}/file`; - } - - // Get image thumbnail URL - getThumbnailUrl(imageId, size = 'medium') { - this.updateConfig(); - return `${this.baseUrl}/api/v1/images/${imageId}/thumbnail?size=${size}`; - } } // Global API client instance diff --git a/client/js/images.js b/client/js/images.js index ceaf52a..bcd9329 100644 --- a/client/js/images.js +++ b/client/js/images.js @@ -48,14 +48,14 @@ function displayImages(images) { const imagesHtml = images.map(image => `
- ${escapeHtml(image.description || 'Image')}
${escapeHtml(truncateText(image.description || 'Untitled', 50))}

- ${formatDate(image.created_at)} + ${formatDate(image.upload_date)}

${image.tags && image.tags.length > 0 ? `
@@ -212,7 +212,7 @@ async function uploadImage() { formData.append('file', file); formData.append('description', description); if (tags.length > 0) { - formData.append('tags', JSON.stringify(tags)); + formData.append('tags', tags.join(',')); } await apiClient.uploadImage(formData); @@ -238,7 +238,7 @@ async function viewImage(imageId) { const modalBody = `
-
@@ -248,9 +248,9 @@ async function viewImage(imageId) {
Details
-

Created: ${formatDate(image.created_at)}

+

Created: ${formatDate(image.upload_date)}

Size: ${formatFileSize(image.file_size)}

-

Dimensions: ${image.width} × ${image.height}

+

Type: ${image.content_type}

${image.tags && image.tags.length > 0 ? ` @@ -287,7 +287,7 @@ async function editImage(imageId) { const modalBody = `
-
diff --git a/client/js/search.js b/client/js/search.js index 1dfb1a4..5956b85 100644 --- a/client/js/search.js +++ b/client/js/search.js @@ -86,7 +86,7 @@ function displaySearchResults(results, query) {
- ${escapeHtml(result.image.description || 'Image')}
${escapeHtml(truncateText(result.image.description || 'Untitled', 60))}

- ${formatDate(result.image.created_at)} + ${formatDate(result.image.upload_date)}

${result.image.tags && result.image.tags.length > 0 ? `
diff --git a/deployment/terraform/pubsub.tf b/deployment/terraform/pubsub.tf index 27f4134..4ed9393 100644 --- a/deployment/terraform/pubsub.tf +++ b/deployment/terraform/pubsub.tf @@ -1,3 +1,14 @@ +# Compute default service accounts +locals { + cloud_run_service_account = var.cloud_run_service_account != "" ? var.cloud_run_service_account : "${data.google_project.current.number}-compute@developer.gserviceaccount.com" + cloud_function_service_account = var.cloud_function_service_account != "" ? var.cloud_function_service_account : "${data.google_project.current.number}-compute@developer.gserviceaccount.com" +} + +# Get current project information +data "google_project" "current" { + project_id = var.project_id +} + # Pub/Sub topic for image processing tasks resource "google_pubsub_topic" "image_processing" { name = var.pubsub_topic_name @@ -73,7 +84,7 @@ resource "google_pubsub_topic_iam_binding" "image_processing_publisher" { role = "roles/pubsub.publisher" members = [ - "serviceAccount:${var.cloud_run_service_account}", + "serviceAccount:${local.cloud_run_service_account}", ] } @@ -83,7 +94,7 @@ resource "google_pubsub_subscription_iam_binding" "image_processing_subscriber" role = "roles/pubsub.subscriber" members = [ - "serviceAccount:${var.cloud_function_service_account}", + "serviceAccount:${local.cloud_function_service_account}", ] } diff --git a/deployment/terraform/terraform.tfstate b/deployment/terraform/terraform.tfstate index 84b0c9e..1349896 100644 --- a/deployment/terraform/terraform.tfstate +++ b/deployment/terraform/terraform.tfstate @@ -1,9 +1,371 @@ { "version": 4, "terraform_version": "1.10.1", - "serial": 28, + "serial": 41, "lineage": "a183cd95-f987-8698-c6dd-84e933c394a5", - "outputs": {}, - "resources": [], + "outputs": { + "container_registry_url": { + "value": "gcr.io/gen-lang-client-0424120530/sereact", + "type": "string" + }, + "pubsub_dlq_topic_name": { + "value": "image-processing-topic-dlq", + "type": "string" + }, + "pubsub_subscription_name": { + "value": "image-processing-topic-subscription", + "type": "string" + }, + "pubsub_topic_name": { + "value": "image-processing-topic", + "type": "string" + }, + "storage_bucket_name": { + "value": "sereact-storage-bucket", + "type": "string" + } + }, + "resources": [ + { + "mode": "data", + "type": "google_project", + "name": "current", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "auto_create_network": null, + "billing_account": "00CA97-62E5BD-4A62B5", + "deletion_policy": "PREVENT", + "effective_labels": { + "generative-language": "enabled" + }, + "folder_id": null, + "id": "projects/gen-lang-client-0424120530", + "labels": { + "generative-language": "enabled" + }, + "name": "Gemini API", + "number": "761163285547", + "org_id": null, + "project_id": "gen-lang-client-0424120530", + "tags": null, + "terraform_labels": { + "generative-language": "enabled" + } + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "managed", + "type": "google_project_service", + "name": "services", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "index_key": "cloudresourcemanager.googleapis.com", + "schema_version": 0, + "attributes": { + "disable_dependent_services": null, + "disable_on_destroy": false, + "id": "gen-lang-client-0424120530/cloudresourcemanager.googleapis.com", + "project": "gen-lang-client-0424120530", + "service": "cloudresourcemanager.googleapis.com", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" + }, + { + "index_key": "containerregistry.googleapis.com", + "schema_version": 0, + "attributes": { + "disable_dependent_services": null, + "disable_on_destroy": false, + "id": "gen-lang-client-0424120530/containerregistry.googleapis.com", + "project": "gen-lang-client-0424120530", + "service": "containerregistry.googleapis.com", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" + }, + { + "index_key": "firestore.googleapis.com", + "schema_version": 0, + "attributes": { + "disable_dependent_services": null, + "disable_on_destroy": false, + "id": "gen-lang-client-0424120530/firestore.googleapis.com", + "project": "gen-lang-client-0424120530", + "service": "firestore.googleapis.com", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" + }, + { + "index_key": "run.googleapis.com", + "schema_version": 0, + "attributes": { + "disable_dependent_services": null, + "disable_on_destroy": false, + "id": "gen-lang-client-0424120530/run.googleapis.com", + "project": "gen-lang-client-0424120530", + "service": "run.googleapis.com", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" + }, + { + "index_key": "storage.googleapis.com", + "schema_version": 0, + "attributes": { + "disable_dependent_services": null, + "disable_on_destroy": false, + "id": "gen-lang-client-0424120530/storage.googleapis.com", + "project": "gen-lang-client-0424120530", + "service": "storage.googleapis.com", + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" + } + ] + }, + { + "mode": "managed", + "type": "google_pubsub_subscription", + "name": "image_processing_dlq", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "ack_deadline_seconds": 10, + "bigquery_config": [], + "cloud_storage_config": [], + "dead_letter_policy": [], + "effective_labels": { + "component": "image-processing-dlq", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "enable_exactly_once_delivery": false, + "enable_message_ordering": false, + "expiration_policy": [ + { + "ttl": "2678400s" + } + ], + "filter": "", + "id": "projects/gen-lang-client-0424120530/subscriptions/image-processing-topic-dlq-subscription", + "labels": { + "component": "image-processing-dlq", + "environment": "dev", + "service": "sereact" + }, + "message_retention_duration": "2592000s", + "name": "image-processing-topic-dlq-subscription", + "project": "gen-lang-client-0424120530", + "push_config": [], + "retain_acked_messages": true, + "retry_policy": [], + "terraform_labels": { + "component": "image-processing-dlq", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "timeouts": null, + "topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic-dlq" + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", + "dependencies": [ + "google_pubsub_topic.image_processing_dlq" + ] + } + ] + }, + { + "mode": "managed", + "type": "google_pubsub_topic", + "name": "image_processing", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "effective_labels": { + "component": "image-processing", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "id": "projects/gen-lang-client-0424120530/topics/image-processing-topic", + "ingestion_data_source_settings": [], + "kms_key_name": "", + "labels": { + "component": "image-processing", + "environment": "dev", + "service": "sereact" + }, + "message_retention_duration": "", + "message_storage_policy": [], + "name": "image-processing-topic", + "project": "gen-lang-client-0424120530", + "schema_settings": [], + "terraform_labels": { + "component": "image-processing", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" + } + ] + }, + { + "mode": "managed", + "type": "google_pubsub_topic", + "name": "image_processing_dlq", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "effective_labels": { + "component": "image-processing-dlq", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "id": "projects/gen-lang-client-0424120530/topics/image-processing-topic-dlq", + "ingestion_data_source_settings": [], + "kms_key_name": "", + "labels": { + "component": "image-processing-dlq", + "environment": "dev", + "service": "sereact" + }, + "message_retention_duration": "", + "message_storage_policy": [], + "name": "image-processing-topic-dlq", + "project": "gen-lang-client-0424120530", + "schema_settings": [], + "terraform_labels": { + "component": "image-processing-dlq", + "environment": "dev", + "goog-terraform-provisioned": "true", + "service": "sereact" + }, + "timeouts": null + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" + } + ] + }, + { + "mode": "managed", + "type": "google_pubsub_topic_iam_binding", + "name": "image_processing_publisher", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "condition": [], + "etag": "BwY14zaElfE=", + "id": "projects/gen-lang-client-0424120530/topics/image-processing-topic/roles/pubsub.publisher", + "members": [ + "serviceAccount:761163285547-compute@developer.gserviceaccount.com" + ], + "project": "gen-lang-client-0424120530", + "role": "roles/pubsub.publisher", + "topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "data.google_project.current", + "google_pubsub_topic.image_processing" + ] + } + ] + }, + { + "mode": "managed", + "type": "google_storage_bucket", + "name": "app_bucket", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 3, + "attributes": { + "autoclass": [], + "cors": [], + "custom_placement_config": [], + "default_event_based_hold": false, + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_object_retention": false, + "encryption": [], + "force_destroy": false, + "hierarchical_namespace": [ + { + "enabled": false + } + ], + "id": "sereact-storage-bucket", + "labels": null, + "lifecycle_rule": [], + "location": "US-CENTRAL1", + "logging": [], + "name": "sereact-storage-bucket", + "project": "gen-lang-client-0424120530", + "project_number": 761163285547, + "public_access_prevention": "inherited", + "requester_pays": false, + "retention_policy": [], + "rpo": null, + "self_link": "https://www.googleapis.com/storage/v1/b/sereact-storage-bucket", + "soft_delete_policy": [ + { + "effective_time": "2025-05-24T15:17:40.181Z", + "retention_duration_seconds": 604800 + } + ], + "storage_class": "STANDARD", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "time_created": "2025-05-24T15:17:40.181Z", + "timeouts": null, + "uniform_bucket_level_access": true, + "updated": "2025-05-24T15:17:40.181Z", + "url": "gs://sereact-storage-bucket", + "versioning": [], + "website": [] + }, + "sensitive_attributes": [], + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsInJlYWQiOjI0MDAwMDAwMDAwMCwidXBkYXRlIjoyNDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjMifQ==", + "dependencies": [ + "google_project_service.services" + ] + } + ] + } + ], "check_results": null } diff --git a/deployment/terraform/terraform.tfstate.backup b/deployment/terraform/terraform.tfstate.backup index 9652ce7..84b0c9e 100644 --- a/deployment/terraform/terraform.tfstate.backup +++ b/deployment/terraform/terraform.tfstate.backup @@ -1,416 +1,9 @@ { "version": 4, "terraform_version": "1.10.1", - "serial": 18, + "serial": 28, "lineage": "a183cd95-f987-8698-c6dd-84e933c394a5", - "outputs": { - "cloud_run_url": { - "value": "https://sereact-p64zpdtkta-uc.a.run.app", - "type": "string" - }, - "container_registry_url": { - "value": "gcr.io/gen-lang-client-0424120530/sereact", - "type": "string" - }, - "firestore_database_id": { - "value": "projects/gen-lang-client-0424120530/databases/imagedb", - "type": "string" - }, - "storage_bucket_name": { - "value": "sereact-storage-bucket", - "type": "string" - } - }, - "resources": [ - { - "mode": "managed", - "type": "google_cloud_run_service", - "name": "sereact", - "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", - "instances": [ - { - "schema_version": 2, - "attributes": { - "autogenerate_revision_name": false, - "id": "locations/us-central1/namespaces/gen-lang-client-0424120530/services/sereact", - "location": "us-central1", - "metadata": [ - { - "annotations": {}, - "effective_annotations": { - "run.googleapis.com/ingress": "all", - "run.googleapis.com/ingress-status": "all", - "run.googleapis.com/operation-id": "0982194f-b3e8-45b8-a33f-7ed5fd529307", - "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" - }, - "effective_labels": { - "cloud.googleapis.com/location": "us-central1" - }, - "generation": 1, - "labels": {}, - "namespace": "gen-lang-client-0424120530", - "resource_version": "AAY108GXDwM", - "self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact", - "terraform_labels": {}, - "uid": "5b695749-5095-4fb5-86bd-4d86f77dc7da" - } - ], - "name": "sereact", - "project": "gen-lang-client-0424120530", - "status": [ - { - "conditions": [ - { - "message": "", - "reason": "", - "status": "True", - "type": "Ready" - }, - { - "message": "", - "reason": "", - "status": "True", - "type": "ConfigurationsReady" - }, - { - "message": "", - "reason": "", - "status": "True", - "type": "RoutesReady" - } - ], - "latest_created_revision_name": "sereact-00001-bt2", - "latest_ready_revision_name": "sereact-00001-bt2", - "observed_generation": 1, - "traffic": [ - { - "latest_revision": true, - "percent": 100, - "revision_name": "sereact-00001-bt2", - "tag": "", - "url": "" - } - ], - "url": "https://sereact-p64zpdtkta-uc.a.run.app" - } - ], - "template": [ - { - "metadata": [ - { - "annotations": { - "autoscaling.knative.dev/maxScale": "3" - }, - "generation": 0, - "labels": { - "run.googleapis.com/startupProbeType": "Default" - }, - "name": "", - "namespace": "", - "resource_version": "", - "self_link": "", - "uid": "" - } - ], - "spec": [ - { - "container_concurrency": 80, - "containers": [ - { - "args": null, - "command": null, - "env": [ - { - "name": "DATABASE_NAME", - "value": "imagedb", - "value_from": [] - }, - { - "name": "GCS_BUCKET_NAME", - "value": "sereact-storage-bucket", - "value_from": [] - }, - { - "name": "LOG_LEVEL", - "value": "INFO", - "value_from": [] - } - ], - "env_from": [], - "image": "gcr.io/google-samples/hello-app:1.0", - "liveness_probe": [], - "name": "", - "ports": [ - { - "container_port": 8080, - "name": "http1", - "protocol": "" - } - ], - "resources": [ - { - "limits": { - "cpu": "1", - "memory": "512Mi" - }, - "requests": null - } - ], - "startup_probe": [ - { - "failure_threshold": 1, - "grpc": [], - "http_get": [], - "initial_delay_seconds": 0, - "period_seconds": 240, - "tcp_socket": [ - { - "port": 8080 - } - ], - "timeout_seconds": 240 - } - ], - "volume_mounts": [], - "working_dir": "" - } - ], - "node_selector": null, - "service_account_name": "761163285547-compute@developer.gserviceaccount.com", - "serving_state": "", - "timeout_seconds": 300, - "volumes": [] - } - ] - } - ], - "timeouts": null, - "traffic": [ - { - "latest_revision": true, - "percent": 100, - "revision_name": "", - "tag": "", - "url": "" - } - ] - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9", - "dependencies": [ - "google_project_service.services" - ] - } - ] - }, - { - "mode": "managed", - "type": "google_cloud_run_service_iam_member", - "name": "public_access", - "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "condition": [], - "etag": "BwY108GyUf0=", - "id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers", - "location": "us-central1", - "member": "allUsers", - "project": "gen-lang-client-0424120530", - "role": "roles/run.invoker", - "service": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact" - }, - "sensitive_attributes": [], - "private": "bnVsbA==", - "dependencies": [ - "google_cloud_run_service.sereact", - "google_project_service.services" - ] - } - ] - }, - { - "mode": "managed", - "type": "google_firestore_database", - "name": "database", - "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "app_engine_integration_mode": "DISABLED", - "cmek_config": [], - "concurrency_mode": "PESSIMISTIC", - "create_time": "", - "database_edition": "STANDARD", - "delete_protection_state": "DELETE_PROTECTION_DISABLED", - "deletion_policy": "ABANDON", - "earliest_version_time": "2025-05-23T20:47:31.801717Z", - "etag": "IIelp4e8uo0DMPX0nai7uo0D", - "id": "projects/gen-lang-client-0424120530/databases/imagedb", - "key_prefix": "", - "location_id": "us-central1", - "name": "imagedb", - "point_in_time_recovery_enablement": "POINT_IN_TIME_RECOVERY_DISABLED", - "project": "gen-lang-client-0424120530", - "timeouts": null, - "type": "FIRESTORE_NATIVE", - "uid": "177e30f2-9b67-428c-9979-22e625b929c4", - "update_time": "", - "version_retention_period": "3600s" - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19", - "dependencies": [ - "google_project_service.services" - ] - } - ] - }, - { - "mode": "managed", - "type": "google_project_service", - "name": "services", - "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", - "instances": [ - { - "index_key": "cloudresourcemanager.googleapis.com", - "schema_version": 0, - "attributes": { - "disable_dependent_services": null, - "disable_on_destroy": false, - "id": "gen-lang-client-0424120530/cloudresourcemanager.googleapis.com", - "project": "gen-lang-client-0424120530", - "service": "cloudresourcemanager.googleapis.com", - "timeouts": null - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" - }, - { - "index_key": "containerregistry.googleapis.com", - "schema_version": 0, - "attributes": { - "disable_dependent_services": null, - "disable_on_destroy": false, - "id": "gen-lang-client-0424120530/containerregistry.googleapis.com", - "project": "gen-lang-client-0424120530", - "service": "containerregistry.googleapis.com", - "timeouts": null - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" - }, - { - "index_key": "firestore.googleapis.com", - "schema_version": 0, - "attributes": { - "disable_dependent_services": null, - "disable_on_destroy": false, - "id": "gen-lang-client-0424120530/firestore.googleapis.com", - "project": "gen-lang-client-0424120530", - "service": "firestore.googleapis.com", - "timeouts": null - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" - }, - { - "index_key": "run.googleapis.com", - "schema_version": 0, - "attributes": { - "disable_dependent_services": null, - "disable_on_destroy": false, - "id": "gen-lang-client-0424120530/run.googleapis.com", - "project": "gen-lang-client-0424120530", - "service": "run.googleapis.com", - "timeouts": null - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" - }, - { - "index_key": "storage.googleapis.com", - "schema_version": 0, - "attributes": { - "disable_dependent_services": null, - "disable_on_destroy": false, - "id": "gen-lang-client-0424120530/storage.googleapis.com", - "project": "gen-lang-client-0424120530", - "service": "storage.googleapis.com", - "timeouts": null - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=" - } - ] - }, - { - "mode": "managed", - "type": "google_storage_bucket", - "name": "app_bucket", - "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", - "instances": [ - { - "schema_version": 3, - "attributes": { - "autoclass": [], - "cors": [], - "custom_placement_config": [], - "default_event_based_hold": false, - "effective_labels": { - "goog-terraform-provisioned": "true" - }, - "enable_object_retention": false, - "encryption": [], - "force_destroy": false, - "hierarchical_namespace": [ - { - "enabled": false - } - ], - "id": "sereact-storage-bucket", - "labels": {}, - "lifecycle_rule": [], - "location": "US-CENTRAL1", - "logging": [], - "name": "sereact-storage-bucket", - "project": "gen-lang-client-0424120530", - "project_number": 761163285547, - "public_access_prevention": "inherited", - "requester_pays": false, - "retention_policy": [], - "rpo": null, - "self_link": "https://www.googleapis.com/storage/v1/b/sereact-storage-bucket", - "soft_delete_policy": [ - { - "effective_time": "2025-05-23T20:47:26.788Z", - "retention_duration_seconds": 604800 - } - ], - "storage_class": "STANDARD", - "terraform_labels": { - "goog-terraform-provisioned": "true" - }, - "time_created": "2025-05-23T20:47:26.788Z", - "timeouts": null, - "uniform_bucket_level_access": true, - "updated": "2025-05-23T20:47:26.788Z", - "url": "gs://sereact-storage-bucket", - "versioning": [], - "website": [] - }, - "sensitive_attributes": [], - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsInJlYWQiOjI0MDAwMDAwMDAwMCwidXBkYXRlIjoyNDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjMifQ==", - "dependencies": [ - "google_project_service.services" - ] - } - ] - } - ], + "outputs": {}, + "resources": [], "check_results": null } diff --git a/deployment/terraform/variables.tf b/deployment/terraform/variables.tf index 120a5ae..ea8d231 100644 --- a/deployment/terraform/variables.tf +++ b/deployment/terraform/variables.tf @@ -41,9 +41,11 @@ variable "pubsub_topic_name" { variable "cloud_run_service_account" { description = "The service account email for Cloud Run" type = string + default = "" } variable "cloud_function_service_account" { description = "The service account email for Cloud Functions" type = string + default = "" } \ No newline at end of file diff --git a/src/api/v1/images.py b/src/api/v1/images.py index 79240f1..be35d61 100644 --- a/src/api/v1/images.py +++ b/src/api/v1/images.py @@ -62,6 +62,9 @@ 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: @@ -74,6 +77,7 @@ async def upload_image( file_size=file_size, content_type=content_type, storage_path=storage_path, + public_url=public_url, team_id=current_user.team_id, uploader_id=current_user.id, description=description, @@ -105,6 +109,7 @@ async def upload_image( file_size=created_image.file_size, content_type=created_image.content_type, storage_path=created_image.storage_path, + public_url=created_image.public_url, team_id=str(created_image.team_id), uploader_id=str(created_image.uploader_id), upload_date=created_image.upload_date, @@ -163,6 +168,19 @@ 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) + response_images.append(ImageResponse( id=str(image.id), filename=image.filename, @@ -170,6 +188,7 @@ async def list_images( file_size=image.file_size, content_type=image.content_type, storage_path=image.storage_path, + public_url=public_url, team_id=str(image.team_id), uploader_id=str(image.uploader_id), upload_date=image.upload_date, @@ -214,6 +233,19 @@ async def get_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) + # Convert to response response = ImageResponse( id=str(image.id), @@ -222,6 +254,7 @@ async def get_image( file_size=image.file_size, content_type=image.content_type, storage_path=image.storage_path, + public_url=public_url, team_id=str(image.team_id), uploader_id=str(image.uploader_id), upload_date=image.upload_date, diff --git a/src/services/storage.py b/src/services/storage.py index 0611dff..f398af7 100644 --- a/src/services/storage.py +++ b/src/services/storage.py @@ -122,7 +122,10 @@ class StorageService: # Upload the file blob.upload_from_string(content, content_type=content_type) - logger.info(f"File uploaded: {storage_path}") + # Make the blob publicly readable + blob.make_public() + + logger.info(f"File uploaded and made public: {storage_path}") # Seek back to the beginning for future reads await file.seek(0) @@ -222,6 +225,29 @@ class StorageService: except Exception as e: logger.error(f"Error generating signed URL: {e}") raise + + def make_file_public(self, storage_path: str) -> bool: + """ + Make a file publicly accessible + + Args: + storage_path: Storage path of the file + + Returns: + True if file was made public, False if not found + """ + try: + blob = self.bucket.blob(storage_path) + if not blob.exists(): + logger.warning(f"File not found for making public: {storage_path}") + return False + + blob.make_public() + logger.info(f"File made public: {storage_path}") + return True + except Exception as e: + logger.error(f"Error making file public: {e}") + raise # Create a singleton service storage_service = StorageService() \ No newline at end of file diff --git a/test_images_api.py b/test_images_api.py new file mode 100644 index 0000000..ae45b2e --- /dev/null +++ b/test_images_api.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +import requests +import json + +# Test the images API +def test_images_api(): + base_url = "http://localhost:8000" + + # First, let's check if we need to bootstrap or if there are existing API keys + try: + # Try to get images without API key first to see the error + response = requests.get(f"{base_url}/api/v1/images") + print(f"Images API without auth: {response.status_code}") + print(f"Response: {response.text[:200]}...") + + except Exception as e: + print(f"Error testing images API: {e}") + +if __name__ == "__main__": + test_images_api() \ No newline at end of file