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
+24
View File
@@ -0,0 +1,24 @@
"""AuthProvider interface.
Operators enable any subset of providers (local, OIDC, social). A provider's
job is narrow: verify a credential and return the matching User (or None).
Session issuance, tokens, and registration live in the auth service and are
provider-agnostic, so adding OIDC/social later (Phase 5) is additive.
"""
from abc import ABC, abstractmethod
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
class AuthProvider(ABC):
name: str
@abstractmethod
async def authenticate(
self, session: AsyncSession, *, identifier: str, secret: str
) -> User | None:
"""Return the User if the credential is valid, else None."""
raise NotImplementedError