Files
Davide Scaini 1b4f0318e7 feat: self-service password reset via email (Phase 4)
- email column on users (migration-safe ALTER TABLE)
- email_reset_tokens table (1h TTL, single-use)
- smtp.py: send via STARTTLS, config from CLI/env vars
- POST /api/auth/request-reset — sends reset link, always 200 (no email leak)
- POST /api/auth/reset-password-token — consumes email token
- GET/POST /api/me/email — users can register/update their email
- reset-password page: email form primary, admin code form as toggle,
  token form shown automatically when ?token= is in URL
- CLI: --smtp-host/port/user/password/from (BINCIO_SMTP_* env vars)
2026-06-03 16:03:08 +02:00

49 lines
1.4 KiB
Python

"""SMTP email sending for bincio-auth.
Config is set by the CLI before uvicorn starts (same pattern as deps.py).
"""
from __future__ import annotations
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# ── Config (set by CLI) ───────────────────────────────────────────────────────
host: str = ""
port: int = 587
user: str = ""
password: str = ""
sender: str = "" # "From" address; defaults to user
def configured() -> bool:
return bool(host and user and password)
def send_reset_email(to_email: str, handle: str, reset_url: str) -> None:
if not configured():
raise RuntimeError("SMTP not configured")
from_addr = sender or user
msg = MIMEMultipart("alternative")
msg["Subject"] = "Reset your Bincio password"
msg["From"] = f"Bincio <{from_addr}>"
msg["To"] = to_email
body = (
f"Hi @{handle},\n\n"
f"Someone requested a password reset for your Bincio account.\n"
f"Click the link below to set a new password — it expires in 1 hour:\n\n"
f" {reset_url}\n\n"
f"If you didn't request this, you can ignore this email.\n\n"
f"— Bincio"
)
msg.attach(MIMEText(body, "plain"))
with smtplib.SMTP(host, port, timeout=10) as s:
s.starttls()
s.login(user, password)
s.send_message(msg)