fix showing images on client
This commit is contained in:
parent
cc2810bbf9
commit
273b20a76c
@ -148,6 +148,37 @@ class ApiClient {
|
||||
return this.makeRequest('DELETE', `/images/${imageId}`);
|
||||
}
|
||||
|
||||
// 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
|
||||
async searchImages(query, similarityThreshold = 0.7, maxResults = 20, tags = null) {
|
||||
const searchData = {
|
||||
|
||||
@ -2,6 +2,34 @@
|
||||
|
||||
let currentPage = 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
|
||||
async function loadImages(page = 1, tags = null) {
|
||||
@ -29,7 +57,7 @@ async function loadImages(page = 1, tags = null) {
|
||||
}
|
||||
|
||||
// Display images in grid
|
||||
function displayImages(images) {
|
||||
async function displayImages(images) {
|
||||
const container = document.getElementById('imagesContainer');
|
||||
|
||||
if (!images || images.length === 0) {
|
||||
@ -46,12 +74,20 @@ function displayImages(images) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create image cards with placeholder images first
|
||||
const imagesHtml = images.map(image => `
|
||||
<div class="image-card card">
|
||||
<img src="${image.public_url || '/placeholder-image.png'}"
|
||||
<div class="image-container" style="position: relative; height: 200px; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center;">
|
||||
<img id="img-${image.id}"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InRyYW5zcGFyZW50Ii8+PC9zdmc+"
|
||||
alt="${escapeHtml(image.description || 'Image')}"
|
||||
onclick="viewImage('${image.id}')"
|
||||
style="cursor: pointer;">
|
||||
style="cursor: pointer; opacity: 0; width: 100%; height: 200px; object-fit: cover; position: absolute; top: 0; left: 0;">
|
||||
<div class="loading-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2;">
|
||||
<div class="loading-spinner large"></div>
|
||||
<div class="mt-2 text-muted small">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${escapeHtml(truncateText(image.description || 'Untitled', 50))}</h6>
|
||||
<p class="card-text small text-muted">
|
||||
@ -78,6 +114,37 @@ function displayImages(images) {
|
||||
`).join('');
|
||||
|
||||
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
|
||||
@ -234,12 +301,31 @@ async function uploadImage() {
|
||||
// View image details
|
||||
async function viewImage(imageId) {
|
||||
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);
|
||||
|
||||
// Load the image with authentication
|
||||
const imageSrc = await loadImageBlob(imageId);
|
||||
|
||||
// Use unique modal ID to prevent conflicts
|
||||
const modalId = `viewImageModal-${imageId}`;
|
||||
|
||||
const modalBody = `
|
||||
<div class="text-center mb-3">
|
||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
||||
style="max-height: 400px;">
|
||||
<img src="${imageSrc}" class="img-fluid rounded"
|
||||
style="max-height: 400px;" alt="${escapeHtml(image.description || 'Image')}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -271,7 +357,14 @@ async function viewImage(imageId) {
|
||||
</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();
|
||||
|
||||
} catch (error) {
|
||||
@ -282,21 +375,37 @@ async function viewImage(imageId) {
|
||||
// Edit image
|
||||
async function editImage(imageId) {
|
||||
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);
|
||||
|
||||
// Load the image with authentication
|
||||
const imageSrc = await loadImageBlob(imageId);
|
||||
|
||||
// Use unique modal ID to prevent conflicts
|
||||
const modalId = `editImageModal-${imageId}`;
|
||||
|
||||
const modalBody = `
|
||||
<form id="editImageForm">
|
||||
<form id="editImageForm-${imageId}">
|
||||
<div class="mb-3 text-center">
|
||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
||||
style="max-height: 200px;">
|
||||
<img src="${imageSrc}" class="img-fluid rounded"
|
||||
style="max-height: 200px;" alt="${escapeHtml(image.description || 'Image')}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="editDescription" rows="3">${escapeHtml(image.description || '')}</textarea>
|
||||
<label for="editDescription-${imageId}" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="editDescription-${imageId}" rows="3">${escapeHtml(image.description || '')}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editTags" class="form-label">Tags</label>
|
||||
<input type="text" class="form-control" id="editTags"
|
||||
<label for="editTags-${imageId}" class="form-label">Tags</label>
|
||||
<input type="text" class="form-control" id="editTags-${imageId}"
|
||||
value="${image.tags ? image.tags.join(', ') : ''}">
|
||||
<div class="form-text">Enter tags separated by commas</div>
|
||||
</div>
|
||||
@ -310,7 +419,14 @@ async function editImage(imageId) {
|
||||
</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();
|
||||
|
||||
} catch (error) {
|
||||
@ -320,11 +436,11 @@ async function editImage(imageId) {
|
||||
|
||||
// Save image changes
|
||||
async function saveImageChanges(imageId) {
|
||||
const description = document.getElementById('editDescription').value.trim();
|
||||
const tagsInput = document.getElementById('editTags').value.trim();
|
||||
const description = document.getElementById(`editDescription-${imageId}`).value.trim();
|
||||
const tagsInput = document.getElementById(`editTags-${imageId}`).value.trim();
|
||||
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);
|
||||
|
||||
try {
|
||||
@ -336,8 +452,8 @@ async function saveImageChanges(imageId) {
|
||||
showAlert('Image updated successfully!', 'success');
|
||||
|
||||
// Close modal and refresh images
|
||||
bootstrap.Modal.getInstance(document.getElementById('editImageModal')).hide();
|
||||
removeModal('editImageModal');
|
||||
bootstrap.Modal.getInstance(document.getElementById(`editImageModal-${imageId}`)).hide();
|
||||
removeModal(`editImageModal-${imageId}`);
|
||||
loadImages(currentPage);
|
||||
|
||||
} catch (error) {
|
||||
@ -355,12 +471,15 @@ function deleteImage(imageId) {
|
||||
showAlert('Image deleted successfully!', 'success');
|
||||
loadImages(currentPage);
|
||||
|
||||
// Close any open modals
|
||||
const modals = ['viewImageModal', 'editImageModal'];
|
||||
modals.forEach(modalId => {
|
||||
// Close any open modals for this image
|
||||
const modalsToClose = [`viewImageModal-${imageId}`, `editImageModal-${imageId}`];
|
||||
modalsToClose.forEach(modalId => {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
if (modalElement) {
|
||||
bootstrap.Modal.getInstance(modalElement)?.hide();
|
||||
const bsModal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (bsModal) {
|
||||
bsModal.hide();
|
||||
}
|
||||
removeModal(modalId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ async function performSearch() {
|
||||
|
||||
try {
|
||||
const results = await apiClient.searchImages(query, threshold, maxResults);
|
||||
displaySearchResults(results, query);
|
||||
await displaySearchResults(results, query);
|
||||
} catch (error) {
|
||||
handleApiError(error, 'searching images');
|
||||
resultsContainer.innerHTML = `
|
||||
@ -57,7 +57,7 @@ async function performSearch() {
|
||||
}
|
||||
|
||||
// Display search results
|
||||
function displaySearchResults(results, query) {
|
||||
async function displaySearchResults(results, query) {
|
||||
const container = document.getElementById('searchResults');
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
@ -65,17 +65,13 @@ function displaySearchResults(results, query) {
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
||||
<h4>No results found</h4>
|
||||
<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>
|
||||
<p class="text-muted">Try adjusting your search query or similarity threshold</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create results HTML with placeholder images first
|
||||
const resultsHtml = `
|
||||
<div class="mb-4">
|
||||
<h4>Search Results</h4>
|
||||
@ -86,11 +82,17 @@ function displaySearchResults(results, query) {
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card search-result h-100">
|
||||
<div class="position-relative">
|
||||
<img src="${result.image.public_url || '/placeholder-image.png'}"
|
||||
<div class="image-container" style="position: relative; height: 200px;">
|
||||
<img id="search-img-${result.image.id}"
|
||||
src="/placeholder-image.png"
|
||||
class="card-img-top"
|
||||
alt="${escapeHtml(result.image.description || 'Image')}"
|
||||
style="height: 200px; object-fit: cover; cursor: pointer;"
|
||||
style="height: 200px; object-fit: cover; cursor: pointer; opacity: 0.5;"
|
||||
onclick="viewImage('${result.image.id}')">
|
||||
<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">
|
||||
<span class="badge bg-primary similarity-score">
|
||||
${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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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); }
|
||||
}
|
||||
|
||||
49
debug_api_key_deployment.py
Normal file
49
debug_api_key_deployment.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to test API key hashing with deployment configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def hash_api_key_manual(api_key: str, secret: str) -> str:
|
||||
"""
|
||||
Manual implementation of API key hashing to test
|
||||
"""
|
||||
return hmac.new(
|
||||
secret.encode(),
|
||||
api_key.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
def test_api_key_hashing():
|
||||
"""Test API key hashing with different secrets"""
|
||||
|
||||
# The new API key from your seeding output
|
||||
test_api_key = "uZBVUEku.7332d85bf6cf2618ad76bca46c2f8125"
|
||||
|
||||
# Test with different possible secrets
|
||||
secrets_to_test = [
|
||||
"super-secret-key-for-development-only", # Default from config.py
|
||||
"development-secret-key-do-not-use-in-production", # Your .env value
|
||||
]
|
||||
|
||||
print("API Key Hashing Debug")
|
||||
print("=" * 60)
|
||||
print(f"Test API Key: {test_api_key}")
|
||||
print()
|
||||
|
||||
for secret in secrets_to_test:
|
||||
hashed_key = hash_api_key_manual(test_api_key, secret)
|
||||
print(f"Secret: {secret}")
|
||||
print(f"Hash: {hashed_key}")
|
||||
print("-" * 60)
|
||||
|
||||
print()
|
||||
print("The deployment should be using one of these hashes in the database.")
|
||||
print("Check your Cloud Run environment variables to confirm which secret is being used.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_api_key_hashing()
|
||||
@ -144,6 +144,11 @@ resource "google_cloud_run_service" "sereact" {
|
||||
name = "LOG_LEVEL"
|
||||
value = "INFO"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "API_KEY_SECRET"
|
||||
value = "super-secret-key-for-development-only"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": [],
|
||||
|
||||
@ -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,
|
||||
|
||||
BIN
deployment/terraform/test_image.jpg
Normal file
BIN
deployment/terraform/test_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to set up Firestore credentials for development and deployment
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def create_env_file(project_id, credentials_file="firestore-credentials.json"):
|
||||
"""Create a .env file with the necessary environment variables"""
|
||||
env_content = f"""# Firestore Settings
|
||||
FIRESTORE_PROJECT_ID={project_id}
|
||||
FIRESTORE_CREDENTIALS_FILE={credentials_file}
|
||||
|
||||
# Google Cloud Storage Settings
|
||||
GCS_BUCKET_NAME={project_id}-storage
|
||||
GCS_CREDENTIALS_FILE={credentials_file}
|
||||
|
||||
# Security settings
|
||||
API_KEY_SECRET=development-secret-key-change-in-production
|
||||
API_KEY_EXPIRY_DAYS=365
|
||||
|
||||
# Vector Database Settings
|
||||
VECTOR_DB_API_KEY=
|
||||
VECTOR_DB_ENVIRONMENT=
|
||||
VECTOR_DB_INDEX_NAME=image-embeddings
|
||||
|
||||
# Other Settings
|
||||
ENVIRONMENT=development
|
||||
LOG_LEVEL=INFO
|
||||
RATE_LIMIT_PER_MINUTE=100
|
||||
"""
|
||||
|
||||
with open(".env", "w") as f:
|
||||
f.write(env_content)
|
||||
|
||||
print("Created .env file with Firestore settings")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Set up Firestore credentials')
|
||||
parser.add_argument('--key-file', type=str, help='Path to the service account key file')
|
||||
parser.add_argument('--project-id', type=str, help='Google Cloud project ID')
|
||||
parser.add_argument('--create-env', action='store_true', help='Create .env file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ensure we have a project ID
|
||||
project_id = args.project_id
|
||||
if not project_id:
|
||||
if args.key_file and os.path.exists(args.key_file):
|
||||
try:
|
||||
with open(args.key_file, 'r') as f:
|
||||
key_data = json.load(f)
|
||||
project_id = key_data.get('project_id')
|
||||
if project_id:
|
||||
print(f"Using project ID from key file: {project_id}")
|
||||
except Exception as e:
|
||||
print(f"Error reading key file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if not project_id:
|
||||
print("Error: Project ID is required")
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Handle key file
|
||||
target_key_file = "firestore-credentials.json"
|
||||
if args.key_file and os.path.exists(args.key_file):
|
||||
# Copy the key file to the target location
|
||||
try:
|
||||
with open(args.key_file, 'r') as src, open(target_key_file, 'w') as dst:
|
||||
key_data = json.load(src)
|
||||
json.dump(key_data, dst, indent=2)
|
||||
print(f"Copied service account key to {target_key_file}")
|
||||
except Exception as e:
|
||||
print(f"Error copying key file: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Warning: No service account key file provided")
|
||||
print(f"You need to place your service account key in {target_key_file}")
|
||||
|
||||
# Create .env file if requested
|
||||
if args.create_env:
|
||||
create_env_file(project_id, target_key_file)
|
||||
|
||||
print("\nSetup complete!")
|
||||
print("\nFor development:")
|
||||
print(f"1. Make sure {target_key_file} exists in the project root")
|
||||
print("2. Ensure environment variables are set in .env file")
|
||||
print("\nFor deployment:")
|
||||
print("1. For Cloud Run, set environment variables in deployment config")
|
||||
print("2. Make sure to securely manage service account key")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -25,6 +25,13 @@ storage_service = StorageService()
|
||||
image_processor = ImageProcessor()
|
||||
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,
|
||||
|
||||
118
tests/test_admin_api.py
Normal file
118
tests/test_admin_api.py
Normal file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify admin image access functionality via API endpoints.
|
||||
This script tests the actual HTTP endpoints to ensure admin users can see all images.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Configuration
|
||||
BASE_URL = "http://localhost:8000"
|
||||
API_BASE = f"{BASE_URL}/api/v1"
|
||||
|
||||
def test_api_endpoints():
|
||||
"""Test the admin functionality via API endpoints"""
|
||||
print("🧪 Testing Admin Image Access via API")
|
||||
print("=" * 50)
|
||||
|
||||
# You'll need to replace these with actual API keys from your system
|
||||
# Get these by running: python scripts/get_test_api_key.py
|
||||
print("📝 To run this test, you need:")
|
||||
print("1. A running API server (python main.py)")
|
||||
print("2. Valid API keys for both admin and regular users")
|
||||
print("3. Update the API_KEYS section below with real keys")
|
||||
print()
|
||||
|
||||
# Example API keys (replace with real ones)
|
||||
API_KEYS = {
|
||||
"admin": "your_admin_api_key_here",
|
||||
"regular": "your_regular_api_key_here"
|
||||
}
|
||||
|
||||
if API_KEYS["admin"] == "your_admin_api_key_here":
|
||||
print("❌ Please update the API_KEYS in this script with real API keys")
|
||||
print(" Run: python scripts/get_test_api_key.py")
|
||||
return
|
||||
|
||||
# Test regular user access
|
||||
print("\n=== Testing Regular User API Access ===")
|
||||
headers_regular = {"X-API-Key": API_KEYS["regular"]}
|
||||
|
||||
try:
|
||||
response = requests.get(f"{API_BASE}/images", headers=headers_regular)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
regular_count = data.get("total", 0)
|
||||
print(f"Regular user sees {regular_count} images")
|
||||
print(f"Images returned: {len(data.get('images', []))}")
|
||||
else:
|
||||
print(f"❌ Regular user API call failed: {response.status_code}")
|
||||
print(response.text)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ Error calling regular user API: {e}")
|
||||
return
|
||||
|
||||
# Test admin user access
|
||||
print("\n=== Testing Admin User API Access ===")
|
||||
headers_admin = {"X-API-Key": API_KEYS["admin"]}
|
||||
|
||||
try:
|
||||
response = requests.get(f"{API_BASE}/images", headers=headers_admin)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
admin_count = data.get("total", 0)
|
||||
print(f"Admin user sees {admin_count} images")
|
||||
print(f"Images returned: {len(data.get('images', []))}")
|
||||
|
||||
# Show teams represented in the results
|
||||
teams = set()
|
||||
for image in data.get('images', []):
|
||||
teams.add(image.get('team_id'))
|
||||
print(f"Images from {len(teams)} different teams")
|
||||
|
||||
else:
|
||||
print(f"❌ Admin user API call failed: {response.status_code}")
|
||||
print(response.text)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ Error calling admin user API: {e}")
|
||||
return
|
||||
|
||||
# Compare results
|
||||
print(f"\n=== Summary ===")
|
||||
print(f"Regular user images: {regular_count}")
|
||||
print(f"Admin user images: {admin_count}")
|
||||
|
||||
if admin_count >= regular_count:
|
||||
print("✅ SUCCESS: Admin sees same or more images than regular user")
|
||||
if admin_count > regular_count:
|
||||
print("✅ PERFECT: Admin sees more images (cross-team access working)")
|
||||
else:
|
||||
print("ℹ️ NOTE: Admin and regular user see same count (might be same team or no other teams)")
|
||||
else:
|
||||
print("❌ FAILURE: Admin should see at least as many images as regular user")
|
||||
|
||||
def get_sample_api_keys():
|
||||
"""Helper function to show how to get API keys"""
|
||||
print("\n📋 How to get API keys for testing:")
|
||||
print("1. Make sure your API server is running:")
|
||||
print(" python main.py")
|
||||
print()
|
||||
print("2. Get a regular user API key:")
|
||||
print(" python scripts/get_test_api_key.py")
|
||||
print()
|
||||
print("3. Get an admin user API key:")
|
||||
print(" python scripts/create_admin.py")
|
||||
print(" # Then use the admin email to get their API key")
|
||||
print()
|
||||
print("4. Update the API_KEYS dictionary in this script")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--help":
|
||||
get_sample_api_keys()
|
||||
else:
|
||||
test_api_endpoints()
|
||||
Loading…
x
Reference in New Issue
Block a user