00bfe8bfca
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>
44 lines
1.6 KiB
Python
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",
|
|
)
|