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