diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc index dd5afa9..250dae2 100644 Binary files a/__pycache__/server.cpython-312.pyc and b/__pycache__/server.cpython-312.pyc differ diff --git a/server.py b/server.py index c0562ce..298f7fc 100644 --- a/server.py +++ b/server.py @@ -60,11 +60,29 @@ async def lifespan(app: FastAPI): # Shutdown logger.info("Application shutdown") +# Environment detection +ENVIRONMENT = os.environ.get('ENVIRONMENT', 'development') +IS_PRODUCTION = ENVIRONMENT == 'production' + +# Security: Disable API documentation in production +if IS_PRODUCTION: + print("🔒 Production mode: API documentation disabled") + app_config = { + "lifespan": lifespan, + "root_path": "/membership", + "docs_url": None, # Disable /docs + "redoc_url": None, # Disable /redoc + "openapi_url": None # Disable /openapi.json + } +else: + print("🔓 Development mode: API documentation enabled at /docs and /redoc") + app_config = { + "lifespan": lifespan, + "root_path": "/membership" + } + # Create the main app -app = FastAPI( - lifespan=lifespan, - root_path="/membership" # Configure for serving under /membership path -) +app = FastAPI(**app_config) # Create a router with the /api prefix api_router = APIRouter(prefix="/api") @@ -790,6 +808,53 @@ async def get_config(): "max_file_size_mb": int(max_file_size_mb) } +@api_router.get("/diagnostics/cors") +async def cors_diagnostics(request: Request): + """ + CORS Diagnostics Endpoint + Shows current CORS configuration and request details for debugging + + Use this to verify: + 1. What origins are allowed + 2. What origin is making the request + 3. Whether CORS is properly configured + """ + cors_origins_env = os.environ.get('CORS_ORIGINS', '') + + if cors_origins_env: + configured_origins = [origin.strip() for origin in cors_origins_env.split(',')] + cors_status = "✅ CONFIGURED" + else: + configured_origins = [ + "http://localhost:3000", + "http://localhost:8000", + "http://127.0.0.1:3000", + "http://127.0.0.1:8000" + ] + cors_status = "⚠️ NOT CONFIGURED (using defaults)" + + request_origin = request.headers.get('origin', 'None') + origin_allowed = request_origin in configured_origins + + return { + "cors_status": cors_status, + "environment": ENVIRONMENT, + "cors_origins_env_variable": cors_origins_env or "(not set)", + "allowed_origins": configured_origins, + "request_origin": request_origin, + "origin_allowed": origin_allowed, + "diagnosis": { + "cors_configured": bool(cors_origins_env), + "origin_matches": origin_allowed, + "issue": None if origin_allowed else f"Origin '{request_origin}' is not in allowed origins list" + }, + "fix_instructions": None if origin_allowed else ( + f"Add to backend .env file:\n" + f"CORS_ORIGINS={request_origin}" + f"{(',' + ','.join(configured_origins)) if cors_origins_env else ''}" + ) + } + # User Profile Routes @api_router.get("/users/profile", response_model=UserResponse) async def get_profile(current_user: User = Depends(get_current_user)): @@ -6236,10 +6301,77 @@ async def stripe_webhook(request: Request, db: Session = Depends(get_db)): # Include the router in the main app app.include_router(api_router) +# ============================================================================ +# MIDDLEWARE CONFIGURATION +# ============================================================================ +# IMPORTANT: In FastAPI, middleware is executed in REVERSE order of addition +# Last added = First executed +# So we add them in this order: Security Headers -> CORS +# Execution order will be: CORS -> Security Headers + +# Security Headers Middleware (Added first, executes second) +@app.middleware("http") +async def add_security_headers(request: Request, call_next): + response = await call_next(request) + + # Security headers to protect against common vulnerabilities + security_headers = { + # Prevent clickjacking attacks + "X-Frame-Options": "DENY", + + # Prevent MIME type sniffing + "X-Content-Type-Options": "nosniff", + + # Enable XSS protection in older browsers + "X-XSS-Protection": "1; mode=block", + + # Control referrer information + "Referrer-Policy": "strict-origin-when-cross-origin", + + # Permissions policy (formerly Feature-Policy) + "Permissions-Policy": "geolocation=(), microphone=(), camera=()", + } + + # Add HSTS header in production (force HTTPS) + if IS_PRODUCTION: + security_headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" + + # Apply all security headers + for header, value in security_headers.items(): + response.headers[header] = value + + # Remove server identification headers (use del, not pop for MutableHeaders) + if "Server" in response.headers: + del response.headers["Server"] + + return response + +print(f"✓ Security headers configured (Production: {IS_PRODUCTION})") + +# CORS Configuration (Added second, executes first) +cors_origins = os.environ.get('CORS_ORIGINS', '') +if cors_origins: + # Use explicitly configured origins + allowed_origins = [origin.strip() for origin in cors_origins.split(',')] +else: + # Default to common development origins if not configured + allowed_origins = [ + "http://localhost:3000", + "http://localhost:8000", + "http://127.0.0.1:3000", + "http://127.0.0.1:8000" + ] + print(f"⚠️ WARNING: CORS_ORIGINS not set. Using defaults: {allowed_origins}") + print("⚠️ For production, set CORS_ORIGINS in .env file!") + +print(f"✓ CORS allowed origins: {allowed_origins}") + app.add_middleware( CORSMiddleware, allow_credentials=True, - allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','), - allow_methods=["*"], + allow_origins=allowed_origins, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], allow_headers=["*"], + expose_headers=["*"], + max_age=600, # Cache preflight requests for 10 minutes ) \ No newline at end of file