diff --git a/README.md b/README.md index 09c6016..fda825f 100644 --- a/README.md +++ b/README.md @@ -193,10 +193,24 @@ The API has CORS (Cross-Origin Resource Sharing) enabled with the following conf - Allowed origins: - http://localhost - http://localhost:3000 + - http://127.0.0.1 + - http://127.0.0.1:3000 - https://v0-ecommerce-app-build-swart.vercel.app + - https://*.vercel.app (for preview deployments) + - * (wildcard for development) - Allowed methods: GET, POST, PUT, DELETE, OPTIONS, PATCH - Allowed headers: Content-Type, Authorization, Accept, Origin, X-Requested-With, X-CSRF-Token, Access-Control-Allow-Credentials - Exposed headers: Content-Length, Content-Type - Credentials support: Enabled -- Max age for preflight requests: 600 seconds (10 minutes) \ No newline at end of file +- Max age for preflight requests: 600 seconds (10 minutes) + +### Custom CORS Handling + +This application uses both FastAPI's built-in CORSMiddleware and a custom CORS middleware that provides enhanced handling of preflight OPTIONS requests. The custom middleware also supports wildcard pattern matching for origins (e.g., https://*.vercel.app) to better support deployment platforms. + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| USE_CUSTOM_CORS_ONLY | Whether to use only the custom CORS middleware | False | \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index e714593..1afb934 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -24,11 +24,25 @@ class Settings(BaseSettings): # CORS settings CORS_ORIGINS: List[str] = [ + # Local development "http://localhost", "http://localhost:3000", - "https://v0-ecommerce-app-build-swart.vercel.app" + "http://127.0.0.1", + "http://127.0.0.1:3000", + + # Vercel frontend + "https://v0-ecommerce-app-build-swart.vercel.app", + + # Other common Vercel domains (in case of redirects or preview deployments) + "https://*.vercel.app", + + # Wildcard as a fallback (less secure but helps with debugging) + "*" ] + # Whether to use only the custom CORS middleware + USE_CUSTOM_CORS_ONLY: bool = False + # Security settings PASSWORD_HASH_ROUNDS: int = 12 diff --git a/main.py b/main.py index 343d545..a3b1853 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,65 @@ import uvicorn -from fastapi import FastAPI +from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware from app.api.v1.api import api_router from app.core.config import settings + +class CustomCORSMiddleware(BaseHTTPMiddleware): + """Custom middleware to ensure CORS headers are set correctly for all responses.""" + + def is_origin_allowed(self, origin: str) -> bool: + """Check if the origin is allowed based on the CORS_ORIGINS settings.""" + if not origin: + return False + + # Direct match + if origin in settings.CORS_ORIGINS: + return True + + # Wildcard match + if "*" in settings.CORS_ORIGINS: + return True + + # Check for pattern matching (e.g., https://*.vercel.app) + for allowed_origin in settings.CORS_ORIGINS: + if "*" in allowed_origin: + pattern = allowed_origin.replace("*", "") + if origin.startswith(pattern.split("*")[0]) and origin.endswith(pattern.split("*")[-1]): + return True + + return False + + async def dispatch(self, request: Request, call_next): + origin = request.headers.get("origin", "") + + if request.method == "OPTIONS": + # Handle preflight requests + response = Response() + + # Check if the origin is allowed + if self.is_origin_allowed(origin): + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS, PATCH" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, Accept, Origin, X-Requested-With, X-CSRF-Token, Access-Control-Allow-Credentials" + response.headers["Access-Control-Allow-Credentials"] = "true" + response.headers["Access-Control-Max-Age"] = "600" + response.status_code = 200 + + return response + + # Process regular requests + response = await call_next(request) + + # Set CORS headers for the response + if self.is_origin_allowed(origin): + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Credentials"] = "true" + + return response + app = FastAPI( title=settings.PROJECT_NAME, description=settings.PROJECT_DESCRIPTION, @@ -14,16 +69,20 @@ app = FastAPI( redoc_url="/redoc", ) -# Set up CORS -app.add_middleware( - CORSMiddleware, - allow_origins=settings.CORS_ORIGINS, - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], - allow_headers=["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token", "Access-Control-Allow-Credentials"], - expose_headers=["Content-Length", "Content-Type"], - max_age=600, # 10 minutes cache for preflight requests -) +# Add our custom CORS middleware first (higher priority) +app.add_middleware(CustomCORSMiddleware) + +# Set up standard CORS middleware as a backup if not disabled +if not settings.USE_CUSTOM_CORS_ONLY: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token", "Access-Control-Allow-Credentials"], + expose_headers=["Content-Length", "Content-Type"], + max_age=600, # 10 minutes cache for preflight requests + ) # Include API router app.include_router(api_router)