Files
rehearshalhub/api/src/rehearsalhub/main.py
Mistral Vibe 68da26588a security: fix auth, CORS, file upload, endpoint hardening + test fixes
- Add INTERNAL_SECRET shared-secret auth to /internal/nc-upload endpoint
- Add JWT token validation to WebSocket /ws/versions/{version_id}
- Fix NameError: band_slug → band.slug in internal.py
- Move inline imports to top of internal.py; add missing Member/NextcloudClient imports
- Remove ~15 debug print() statements from auth.py
- Replace Content-Type-only avatar check with extension whitelist + Pillow Image.verify()
- Sanitize exception details in versions.py (no more str(e) in 4xx/5xx responses)
- Restrict CORS allow_methods/allow_headers from "*" to explicit lists
- Add security headers middleware: X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Reduce JWT expiry from 7 days to 1 hour
- Add Pillow>=10.0 dependency; document INTERNAL_SECRET in .env.example
- Implement missing RedisJobQueue.dequeue() method (required by protocol)
- Fix 5 pre-existing unit test failures: settings env vars conftest, deferred Redis push,
  dequeue method, AsyncMock→MagicMock for sync scalar_one_or_none

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 21:02:56 +02:00

87 lines
2.6 KiB
Python

"""RehearsalHub FastAPI application entry point."""
from contextlib import asynccontextmanager
import os
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from rehearsalhub.config import get_settings
from rehearsalhub.routers import (
annotations_router,
auth_router,
bands_router,
internal_router,
members_router,
sessions_router,
songs_router,
versions_router,
ws_router,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
# Clean up DB connections on shutdown
from rehearsalhub.db.engine import get_engine
engine = get_engine()
await engine.dispose()
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(
title="RehearsalHub API",
version="0.1.0",
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=[f"https://{settings.domain}", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type", "Accept"],
)
@app.middleware("http")
async def security_headers(request: Request, call_next) -> Response:
response = await call_next(request)
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["X-XSS-Protection"] = "0" # Disable legacy XSS auditor; rely on CSP
return response
prefix = "/api/v1"
app.include_router(auth_router, prefix=prefix)
app.include_router(bands_router, prefix=prefix)
app.include_router(sessions_router, prefix=prefix)
app.include_router(songs_router, prefix=prefix)
app.include_router(versions_router, prefix=prefix)
app.include_router(annotations_router, prefix=prefix)
app.include_router(members_router, prefix=prefix)
app.include_router(internal_router, prefix=prefix)
app.include_router(ws_router) # WebSocket routes don't use /api/v1 prefix
@app.get("/api/health")
async def health():
return {"status": "ok"}
# Mount static files for avatar uploads
upload_dir = "uploads/avatars"
os.makedirs(upload_dir, exist_ok=True)
app.mount("/api/static/avatars", StaticFiles(directory=upload_dir), name="avatars")
return app
app = create_app()