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:
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user