Add local auth: AuthProvider, mailer, sessions, /api/v1/auth

Pluggable AuthProvider interface with a local (email+password) implementation, and a Mailer interface (ConsoleMailer for dev, SMTPMailer for operators). The auth service owns registration, login, opaque session issuance, email verification, and password reset (which revokes prior sessions). Endpoints under /api/v1/auth; sessions are returned as a Bearer token and set as an HttpOnly cookie.

Replaces the temporary X-User-Id shim: get_current_user now resolves a real session (Bearer or cookie). The open user-bootstrap endpoint is gone (registration replaces it). App logging is configured so the ConsoleMailer's verification/reset links are visible to self-hosters. Verified end-to-end on the deploy target, including the email-verification flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-06 10:51:51 -04:00
parent 5123c85397
commit 00bfe8bfca
15 changed files with 497 additions and 31 deletions
+17
View File
@@ -5,6 +5,9 @@ OpenAPI contract. All domain logic lives in the service layer; the privacy
engine is the single enforcement point for reads.
"""
import logging
import sys
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
@@ -14,6 +17,19 @@ from app.core.config import get_settings
from app.services.exceptions import Conflict, Forbidden, NotFound
def _configure_logging() -> None:
"""Emit the app's own ``provenance.*`` logs at INFO to stdout (uvicorn only
configures its own loggers). The ConsoleMailer relies on this so self-hosters
can read verification/reset links from the logs."""
app_logger = logging.getLogger("provenance")
app_logger.setLevel(logging.INFO)
if not app_logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(levelname)s [%(name)s] %(message)s"))
app_logger.addHandler(handler)
app_logger.propagate = False
def _register_error_handlers(app: FastAPI) -> None:
@app.exception_handler(NotFound)
async def _not_found(request: Request, exc: NotFound) -> JSONResponse:
@@ -29,6 +45,7 @@ def _register_error_handlers(app: FastAPI) -> None:
def create_app() -> FastAPI:
_configure_logging()
settings = get_settings()
app = FastAPI(
title=settings.app_name,