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:
@@ -0,0 +1,16 @@
|
||||
"""Mailer interface for transactional email.
|
||||
|
||||
Implementations: ConsoleMailer (dev default — logs the link) and SMTPMailer
|
||||
(operator-configured). Selected by config; resolved via app.api.deps.get_mailer.
|
||||
Real deployments will move sending to the worker; for now it is inline.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Mailer(ABC):
|
||||
@abstractmethod
|
||||
async def send_email_verification(self, *, to: str, link: str) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def send_password_reset(self, *, to: str, link: str) -> None: ...
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Development mailer: logs the would-be email (including the action link) to
|
||||
stdout instead of sending. Never use in production."""
|
||||
|
||||
import logging
|
||||
|
||||
from app.integrations.mailer.base import Mailer
|
||||
|
||||
logger = logging.getLogger("provenance.mailer")
|
||||
|
||||
|
||||
class ConsoleMailer(Mailer):
|
||||
async def send_email_verification(self, *, to: str, link: str) -> None:
|
||||
logger.info("[email] verify address for %s -> %s", to, link)
|
||||
|
||||
async def send_password_reset(self, *, to: str, link: str) -> None:
|
||||
logger.info("[email] password reset for %s -> %s", to, link)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""SMTP mailer using the standard library, run off the event loop. Configured
|
||||
entirely from settings (host/port/credentials/from)."""
|
||||
|
||||
import asyncio
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.integrations.mailer.base import Mailer
|
||||
|
||||
|
||||
class SMTPMailer(Mailer):
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
def _send(self, *, to: str, subject: str, body: str) -> None:
|
||||
msg = EmailMessage()
|
||||
msg["From"] = self.settings.smtp_from
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
msg.set_content(body)
|
||||
with smtplib.SMTP(self.settings.smtp_host, self.settings.smtp_port) as smtp:
|
||||
smtp.starttls()
|
||||
if self.settings.smtp_username and self.settings.smtp_password:
|
||||
smtp.login(self.settings.smtp_username, self.settings.smtp_password)
|
||||
smtp.send_message(msg)
|
||||
|
||||
async def _send_async(self, *, to: str, subject: str, body: str) -> None:
|
||||
await asyncio.to_thread(self._send, to=to, subject=subject, body=body)
|
||||
|
||||
async def send_email_verification(self, *, to: str, link: str) -> None:
|
||||
await self._send_async(
|
||||
to=to,
|
||||
subject="Verify your Provenance email",
|
||||
body=f"Confirm your email address:\n\n{link}\n",
|
||||
)
|
||||
|
||||
async def send_password_reset(self, *, to: str, link: str) -> None:
|
||||
await self._send_async(
|
||||
to=to,
|
||||
subject="Reset your Provenance password",
|
||||
body=f"Reset your password:\n\n{link}\n",
|
||||
)
|
||||
Reference in New Issue
Block a user