Spaces:
Paused
Paused
| """ | |
| security.py | |
| =========== | |
| Security layers: | |
| 1. CORS β portfolio domain only | |
| 2. X-Portfolio-Key β shared secret between Vercel proxy and this backend | |
| (secret lives in Vercel server env, never in browser JS) | |
| 3. Rate limiting β slowapi, 10 req/min per IP | |
| """ | |
| import os | |
| import logging | |
| from fastapi import Request, HTTPException | |
| from slowapi import Limiter | |
| from slowapi.util import get_remote_address | |
| logger = logging.getLogger(__name__) | |
| # ββ Secrets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PORTFOLIO_SECRET_KEY = os.environ.get("PORTFOLIO_SECRET_KEY", "") | |
| # ββ Allowed origins βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Add your Vercel domain here. localhost entries are for local dev only. | |
| ALLOWED_ORIGINS = [ | |
| "https://sulitha-nulaksha-portfolio.vercel.app", | |
| "http://localhost:3000", | |
| "http://localhost:5173", | |
| ] | |
| # ββ Rate limiter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| limiter = Limiter(key_func=get_remote_address, default_limits=["10/minute"]) | |
| # ββ Origin token check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def verify_portfolio_key(request: Request) -> None: | |
| """ | |
| Reject requests that don't carry the correct X-Portfolio-Key header. | |
| This header is set server-side by the Vercel API route proxy β it never | |
| reaches the browser, so it cannot be scraped from frontend JS. | |
| Skipped with a warning if PORTFOLIO_SECRET_KEY is not set (useful for | |
| local dev without the full proxy setup). | |
| """ | |
| if not PORTFOLIO_SECRET_KEY: | |
| logger.warning("PORTFOLIO_SECRET_KEY not set β skipping key check (dev mode).") | |
| return | |
| key = request.headers.get("X-Portfolio-Key", "") | |
| if key != PORTFOLIO_SECRET_KEY: | |
| raise HTTPException(status_code=403, detail="Forbidden") | |