""" BreakPilot Core Backend Shared APIs for authentication, RBAC, notifications, email templates, system health, security (DevSecOps), and common middleware. This is the extracted "core" service from the monorepo backend. It runs on port 8000 and uses the `core` schema in PostgreSQL. """ import os import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware # --------------------------------------------------------------------------- # Router imports (shared APIs only) # --------------------------------------------------------------------------- from auth_api import router as auth_router from rbac_api import router as rbac_router from notification_api import router as notification_router from email_template_api import ( router as email_template_router, versions_router as email_template_versions_router, ) from system_api import router as system_router from security_api import router as security_router # --------------------------------------------------------------------------- # Middleware imports # --------------------------------------------------------------------------- from middleware import ( RequestIDMiddleware, SecurityHeadersMiddleware, RateLimiterMiddleware, PIIRedactor, InputGateMiddleware, ) # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- logging.basicConfig( level=os.getenv("LOG_LEVEL", "INFO"), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger("backend-core") # --------------------------------------------------------------------------- # Application # --------------------------------------------------------------------------- app = FastAPI( title="BreakPilot Core Backend", description="Shared APIs: Auth, RBAC, Notifications, Email Templates, System, Security", version="1.0.0", ) # --------------------------------------------------------------------------- # CORS # --------------------------------------------------------------------------- ALLOWED_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",") app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --------------------------------------------------------------------------- # Custom middleware stack (order matters -- outermost first) # --------------------------------------------------------------------------- # 1. Request-ID (outermost so every response has it) app.add_middleware(RequestIDMiddleware) # 2. Security headers app.add_middleware(SecurityHeadersMiddleware) # 3. Input gate (body-size / content-type validation) app.add_middleware(InputGateMiddleware) # 4. Rate limiter (Valkey-backed) VALKEY_URL = os.getenv("VALKEY_URL", os.getenv("REDIS_URL", "redis://valkey:6379/0")) app.add_middleware(RateLimiterMiddleware, valkey_url=VALKEY_URL) # --------------------------------------------------------------------------- # Routers # --------------------------------------------------------------------------- # Auth (proxy to consent-service) app.include_router(auth_router, prefix="/api") # RBAC (teacher / role management) app.include_router(rbac_router, prefix="/api") # Notifications (proxy to consent-service) app.include_router(notification_router, prefix="/api") # Email templates (proxy to consent-service) app.include_router(email_template_router) # already has /api/consent/admin/email-templates prefix app.include_router(email_template_versions_router) # already has /api/consent/admin/email-template-versions prefix # System (health, local-ip) app.include_router(system_router) # already has paths defined in router # Security / DevSecOps dashboard app.include_router(security_router, prefix="/api") # --------------------------------------------------------------------------- # Startup / Shutdown events # --------------------------------------------------------------------------- @app.on_event("startup") async def on_startup(): logger.info("backend-core starting up") # Ensure DATABASE_URL uses search_path=core,public db_url = os.getenv("DATABASE_URL", "") if db_url and "search_path" not in db_url: separator = "&" if "?" in db_url else "?" new_url = f"{db_url}{separator}search_path=core,public" os.environ["DATABASE_URL"] = new_url logger.info("DATABASE_URL updated with search_path=core,public") elif "search_path" in db_url: logger.info("DATABASE_URL already contains search_path") else: logger.warning("DATABASE_URL is not set -- database features will fail") @app.on_event("shutdown") async def on_shutdown(): logger.info("backend-core shutting down") # --------------------------------------------------------------------------- # Entrypoint (for `python main.py` during development) # --------------------------------------------------------------------------- if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host="0.0.0.0", port=int(os.getenv("PORT", "8000")), reload=os.getenv("ENVIRONMENT", "development") == "development", )