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)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user