feat: implement Phase 1 hardening

- Verify JWT signatures via JWKS in auth.py
- Fix broken frontend auth button references
- Add Pydantic Settings for env validation (RETENTION_DAYS, CORS_ORIGINS)
- Create MongoDB indexes + TTL on startup
- Add /health endpoint and CORS middleware
- Escape regex input in event queries
- Fix dedupe() return calculation in maintenance.py
- Replace basic logging with structured structlog JSON logs
- Update README and add ROADMAP.md
This commit is contained in:
2026-04-14 11:48:29 +02:00
parent f9f1399f57
commit 4f6e16d64d
12 changed files with 392 additions and 46 deletions

View File

@@ -2,41 +2,85 @@ import asyncio
import logging
from pathlib import Path
from fastapi import FastAPI
import structlog
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from database import setup_indexes
from routes.fetch import router as fetch_router, run_fetch
from routes.events import router as events_router
from routes.config import router as config_router
from config import ENABLE_PERIODIC_FETCH, FETCH_INTERVAL_MINUTES
from config import ENABLE_PERIODIC_FETCH, FETCH_INTERVAL_MINUTES, CORS_ORIGINS
def configure_logging():
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer(),
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logging.basicConfig(format="%(message)s", level=logging.INFO)
configure_logging()
logger = structlog.get_logger("aoc.fetcher")
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(fetch_router, prefix="/api")
app.include_router(events_router, prefix="/api")
app.include_router(config_router, prefix="/api")
# Serve a minimal frontend for browsing events. Use an absolute path so it
# works regardless of the working directory used to start uvicorn.
@app.get("/health")
async def health_check():
from database import db
try:
db.command("ping")
return {"status": "ok", "database": "connected"}
except Exception as exc:
logger.error("Health check failed", error=str(exc))
raise HTTPException(status_code=503, detail="Database unavailable") from exc
frontend_dir = Path(__file__).parent / "frontend"
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
logger = logging.getLogger("aoc.fetcher")
async def _periodic_fetch():
while True:
try:
await asyncio.to_thread(run_fetch)
logger.info("Periodic fetch completed.")
except Exception as exc:
logger.error("Periodic fetch failed: %s", exc)
logger.error("Periodic fetch failed", error=str(exc))
await asyncio.sleep(FETCH_INTERVAL_MINUTES * 60)
@app.on_event("startup")
async def start_periodic_fetch():
setup_indexes()
if ENABLE_PERIODIC_FETCH:
app.state.fetch_task = asyncio.create_task(_periodic_fetch())