1b4f0318e7
- 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)
49 lines
1.4 KiB
Python
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)
|