From 061f67f9f23703a4eff38b89d8b3bb1d42a5adcf Mon Sep 17 00:00:00 2001
From: Ben Davison <bendavison@Bens-MacBook-Pro.local>
Date: Mon, 12 May 2025 16:37:20 +0100
Subject: [PATCH] Image upload backend

---
 api-gateway/main.py          | 88 ++++++++++++++++++++++++++++++++++--
 api-gateway/requirements.txt |  1 +
 docker-compose.yml           | 18 +++++++-
 frontend/src/utils/images.ts | 32 +++++++++++++
 4 files changed, 135 insertions(+), 4 deletions(-)
 create mode 100644 frontend/src/utils/images.ts

diff --git a/api-gateway/main.py b/api-gateway/main.py
index f2e8dc5..3406bbc 100644
--- a/api-gateway/main.py
+++ b/api-gateway/main.py
@@ -1,6 +1,7 @@
-from fastapi import FastAPI, Request, HTTPException, Depends
+from fastapi import FastAPI, Request, HTTPException, Depends, UploadFile, File
 from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse, Response
+from fastapi.responses import JSONResponse, Response, FileResponse
+from fastapi.staticfiles import StaticFiles
 import httpx
 import json
 import asyncio
@@ -8,9 +9,15 @@ import asyncio
 import jwt
 from typing import Optional
 import os
+import shutil
+from pathlib import Path
 
 app = FastAPI(title="API Gateway")
 
+# Create images directory if it doesn't exist
+IMAGES_DIR = Path("images")
+IMAGES_DIR.mkdir(exist_ok=True)
+
 # JWT settings
 JWT_SECRET = os.getenv("JWT_SECRET", "abc123")
 EXCLUDED_PATHS = [
@@ -89,6 +96,25 @@ async def verify_token(request: Request) -> Optional[dict]:
             headers={"X-Auth-Error": "Invalid token"}
         )
 
+# Helper functions for image handling
+async def save_image(image_key: str, image_file: UploadFile) -> str:
+    """Save an uploaded image to the filesystem"""
+    # Create a safe filename
+    file_extension = os.path.splitext(image_file.filename)[1]
+    safe_filename = f"{image_key}{file_extension}"
+    file_path = IMAGES_DIR / safe_filename
+    
+    # Save the file
+    with open(file_path, "wb") as buffer:
+        shutil.copyfileobj(image_file.file, buffer)
+    
+    return safe_filename
+
+async def delete_image(filename: str):
+    file_path = IMAGES_DIR / filename
+    if file_path.exists():
+        file_path.unlink()
+
 @app.get("/")
 async def root():
     return {"message": "API Gateway is running"}
@@ -107,6 +133,62 @@ async def health_check():
     
     return {"services": results}
 
+# Test upload curl: curl -X POST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbTltcjl1ZzUwMDAwMGNqeDZqcWhkN3oyIiwiaWF0IjoxNzQ3MDU0MjI2LCJleHAiOjE3NDc0ODYyMjZ9.kFgPqgIKNV6XjUK4-uCenngLETWYx4TU63IOJcTH1pk" -F "image=@frontend/public/sports.png" http://localhost:8000/images/upload/test-sports
+# Test delete curl: curl -X DELETE -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbTltcjl1ZzUwMDAwMGNqeDZqcWhkN3oyIiwiaWF0IjoxNzQ3MDU0MjI2LCJleHAiOjE3NDc0ODYyMjZ9.kFgPqgIKNV6XjUK4-uCenngLETWYx4TU63IOJcTH1pk" http://localhost:8000/images/test-sports.png
+# Endpoint for uploading an image
+@app.post("/images/upload/{image_key}")
+async def upload_image(image_key: str, image: UploadFile = File(...), request: Request = None):
+    print(image)
+    try:
+        payload = await verify_token(request)
+        # Add user ID to headers if token is valid
+        if payload and 'userId' in payload:
+            headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
+            headers['X-User-ID'] = payload['userId']
+        else:
+            headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
+    except HTTPException as e:
+        return JSONResponse(
+            status_code=e.status_code,
+            content={"detail": e.detail},
+            headers=e.headers
+        )
+    try:
+        filename = await save_image(image_key, image)
+        return {
+            "message": "Image uploaded successfully",
+            "filename": filename,
+            "url": f"/images/{filename}"
+        }
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+# Endpoint for deleting an image
+@app.delete("/images/{filename}")
+async def delete_image_endpoint(filename: str, request: Request):
+    try:
+        payload = await verify_token(request)
+        # Add user ID to headers if token is valid
+        if payload and 'userId' in payload:
+            headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
+            headers['X-User-ID'] = payload['userId']
+        else:
+            headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
+    except HTTPException as e:
+        return JSONResponse(
+            status_code=e.status_code,
+            content={"detail": e.detail},
+            headers=e.headers
+        )
+    try:
+        await delete_image(filename)
+        return {"message": "Image deleted successfully"}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+# Mount the images directory
+app.mount("/images", StaticFiles(directory="images"), name="images")
+
 @app.api_route("/{service}/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"])
 async def gateway(service: str, path: str, request: Request):
     # Verify token for all requests except excluded paths
@@ -182,4 +264,4 @@ async def gateway(service: str, path: str, request: Request):
                     headers=dict(response.headers)
                 )
         except httpx.RequestError as exc:
-            raise HTTPException(status_code=503, detail=f"Service {service} unavailable") from exc
+            raise HTTPException(status_code=503, detail=f"Service {service} unavailable") from exc
\ No newline at end of file
diff --git a/api-gateway/requirements.txt b/api-gateway/requirements.txt
index 3355545..49a380d 100644
--- a/api-gateway/requirements.txt
+++ b/api-gateway/requirements.txt
@@ -3,3 +3,4 @@ uvicorn
 httpx
 redis
 pyjwt
+python-multipart
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 6e37787..fea2cd4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -24,6 +24,7 @@ services:
       - CORS_ORIGINS=http://localhost:5173
     volumes:
       - ./api-gateway:/app
+      - ./api-gateway/images:/app/images
     networks:
       - microservices-network
     depends_on:
@@ -173,6 +174,20 @@ services:
       timeout: 5s
       retries: 5
 
+  redis:
+    image: redis:7-alpine
+    ports:
+      - "6379:6379"
+    volumes:
+      - redis_data:/data
+    networks:
+      - microservices-network
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 5s
+      timeout: 5s
+      retries: 5
+
 networks:
   microservices-network:
     driver: bridge
@@ -186,4 +201,5 @@ volumes:
   users_postgres_data:
   events_postgres_data:
   reviews_postgres_data:
-  recommendations_mongodb_data: 
+  recommendations_mongodb_data:
+  redis_data: 
diff --git a/frontend/src/utils/images.ts b/frontend/src/utils/images.ts
new file mode 100644
index 0000000..d8cb43f
--- /dev/null
+++ b/frontend/src/utils/images.ts
@@ -0,0 +1,32 @@
+import { getAuthHeaders, handleAuthError } from "./auth";
+
+// Frontend code
+async function uploadImage(imageKey: string, imageFile: File) {
+    const formData = new FormData();
+    formData.append('image', imageFile);
+    
+    const response = await fetch(`/images/upload/${imageKey}`, {
+      method: 'POST',
+      body: formData,
+      headers: getAuthHeaders()
+    });
+
+    if (!response.ok) {
+      handleAuthError();
+    }
+
+    return response.json();
+  }
+
+  async function deleteImage(filename: string) {
+    const response = await fetch(`/images/${filename}`, {
+      method: 'DELETE',
+      headers: getAuthHeaders()
+    });
+
+    if (!response.ok) {
+      handleAuthError();
+    }
+
+    return response.json();
+  }
\ No newline at end of file
-- 
GitLab