diff --git a/src/services/storage_service.py b/src/services/storage_service.py index fa2dcaa..b14dd41 100644 --- a/src/services/storage_service.py +++ b/src/services/storage_service.py @@ -98,6 +98,40 @@ class StorageService: await asyncio.to_thread(_delete) logger.info(f"Deleted image: {file_path}") + @staticmethod + def get_proxy_url(image_url: str, app_base_url: str = "") -> str: + """ + Convert a Supabase storage URL to a proxied HTTPS URL. + + This solves mixed content issues when Supabase storage uses HTTP + but the app uses HTTPS. + + Args: + image_url: The original Supabase storage URL + app_base_url: Base URL of the app (e.g., https://linkedin.onyva.dev) + + Returns: + Proxied URL through the app (e.g., /proxy/image/post-images/...) + """ + if not image_url: + return "" + + # Extract bucket and path from Supabase URL + # Format: http://supabase.../storage/v1/object/public/{bucket}/{path} + parts = image_url.split("/storage/v1/object/public/") + if len(parts) != 2: + return image_url # Return original if format doesn't match + + bucket_and_path = parts[1] # e.g., "post-images/user-id/file.jpg" + parts2 = bucket_and_path.split("/", 1) + if len(parts2) != 2: + return image_url + + bucket, path = parts2 + + # Return proxy URL + return f"{app_base_url}/proxy/image/{bucket}/{path}" + # Global singleton storage = StorageService() diff --git a/src/web/templates/user/base.html b/src/web/templates/user/base.html index 486f322..3f208d4 100644 --- a/src/web/templates/user/base.html +++ b/src/web/templates/user/base.html @@ -252,6 +252,45 @@ })(); + + + {% block scripts %}{% endblock %} diff --git a/src/web/templates/user/company_base.html b/src/web/templates/user/company_base.html index 476e1a4..884ec56 100644 --- a/src/web/templates/user/company_base.html +++ b/src/web/templates/user/company_base.html @@ -109,6 +109,34 @@ + + + {% block scripts %}{% endblock %} diff --git a/src/web/user/routes.py b/src/web/user/routes.py index 25aee86..2ca6a5f 100644 --- a/src/web/user/routes.py +++ b/src/web/user/routes.py @@ -3529,3 +3529,44 @@ async def delete_post_image(request: Request, post_id: str): except Exception as e: logger.exception(f"Failed to delete post image: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== IMAGE PROXY FOR HTTPS ==================== + +@user_router.get("/proxy/image/{bucket}/{path:path}") +async def proxy_supabase_image(bucket: str, path: str): + """ + Proxy Supabase storage images via HTTPS to avoid mixed content warnings. + + This allows HTTPS pages to load images from HTTP Supabase storage. + """ + import httpx + from fastapi.responses import Response + + try: + # Build the Supabase storage URL + storage_url = settings.supabase_url.replace("https://", "http://").replace("http://", "http://") + image_url = f"{storage_url}/storage/v1/object/public/{bucket}/{path}" + + # Fetch the image from Supabase + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(image_url) + + if response.status_code != 200: + raise HTTPException(status_code=404, detail="Image not found") + + # Return the image with proper headers + return Response( + content=response.content, + media_type=response.headers.get("content-type", "image/jpeg"), + headers={ + "Cache-Control": "public, max-age=31536000", + "Access-Control-Allow-Origin": "*" + } + ) + + except httpx.TimeoutException: + raise HTTPException(status_code=504, detail="Image fetch timeout") + except Exception as e: + logger.error(f"Failed to proxy image {bucket}/{path}: {e}") + raise HTTPException(status_code=500, detail="Failed to load image")