Files
justin 00bfe8bfca 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>
2026-06-06 10:51:51 -04:00

44 lines
1.6 KiB
Python

"""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",
)