- Remove external backend port 8000 exposure (VULN-01) - Conditionally disable Swagger/ReDoc/OpenAPI in non-dev (VULN-01) - Suppress nginx and uvicorn server version headers (VULN-07) - Fix CSP to allow Google Fonts (fonts.googleapis.com/gstatic) (VULN-08) - Add nginx rate limiting on auth endpoints (10r/m burst=5) (VULN-09) - Block dotfile access (/.env, /.git) while preserving .well-known (VULN-10) - Make CORS origins configurable, tighten methods/headers (VULN-11) - Run both containers as non-root users (VULN-12) - Add IP rate limit + account lockout to /change-password Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from contextlib import asynccontextmanager
|
|
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
|
|
from app.config import settings
|
|
from app.database import engine
|
|
from app.routers import auth, todos, events, calendars, reminders, projects, people, locations, settings as settings_router, dashboard, weather, event_templates
|
|
from app.routers import totp
|
|
from app.jobs.notifications import run_notification_dispatch
|
|
|
|
# Import models so Alembic's autogenerate can discover them
|
|
from app.models import user as _user_model # noqa: F401
|
|
from app.models import session as _session_model # noqa: F401
|
|
from app.models import totp_usage as _totp_usage_model # noqa: F401
|
|
from app.models import backup_code as _backup_code_model # noqa: F401
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
scheduler = AsyncIOScheduler()
|
|
scheduler.add_job(
|
|
run_notification_dispatch,
|
|
"interval",
|
|
minutes=1,
|
|
id="ntfy_dispatch",
|
|
max_instances=1, # prevent overlap if a run takes longer than 60s
|
|
)
|
|
scheduler.start()
|
|
yield
|
|
scheduler.shutdown(wait=False)
|
|
await engine.dispose()
|
|
|
|
|
|
_is_dev = settings.ENVIRONMENT == "development"
|
|
|
|
app = FastAPI(
|
|
title="UMBRA API",
|
|
description="Backend API for UMBRA application",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
docs_url="/docs" if _is_dev else None,
|
|
redoc_url="/redoc" if _is_dev else None,
|
|
openapi_url="/openapi.json" if _is_dev else None,
|
|
)
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()],
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
allow_headers=["Content-Type", "Authorization", "Cookie", "X-Requested-With"],
|
|
)
|
|
|
|
# Include routers with /api prefix
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
|
app.include_router(todos.router, prefix="/api/todos", tags=["Todos"])
|
|
app.include_router(events.router, prefix="/api/events", tags=["Calendar Events"])
|
|
app.include_router(calendars.router, prefix="/api/calendars", tags=["Calendars"])
|
|
app.include_router(reminders.router, prefix="/api/reminders", tags=["Reminders"])
|
|
app.include_router(projects.router, prefix="/api/projects", tags=["Projects"])
|
|
app.include_router(people.router, prefix="/api/people", tags=["People"])
|
|
app.include_router(locations.router, prefix="/api/locations", tags=["Locations"])
|
|
app.include_router(settings_router.router, prefix="/api/settings", tags=["Settings"])
|
|
app.include_router(dashboard.router, prefix="/api", tags=["Dashboard"])
|
|
app.include_router(weather.router, prefix="/api/weather", tags=["Weather"])
|
|
app.include_router(event_templates.router, prefix="/api/event-templates", tags=["Event Templates"])
|
|
app.include_router(totp.router, prefix="/api/auth", tags=["TOTP MFA"])
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {"message": "UMBRA API is running"}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {"status": "healthy"}
|