Add SMTP email: password reset + per-frame battery-threshold alerts
Build and push server image / build-and-push (push) Successful in 40s
Build and push server image / build-and-push (push) Successful in 40s
Admin-configured SMTP (server/port/username/password/from address/ STARTTLS, a singleton server_settings row set from /admin -- not env vars, since it's operator infrastructure a household admin sets up once through the UI) powers two features, both requiring the relevant user to have an email set in their own Settings: - "Forgot password?" on /login emails a one-hour single-use reset link (password_reset_tokens table). The endpoint always returns the same generic "check your email" response regardless of whether the address matched an account, so it can't be used to enumerate registered users. - A frame's Configuration tab can set a battery-alert threshold (Frame.battery_alert_threshold_pct, -1 = disabled); POST /frame/battery emails the owner the first time a report drops to or below it, then stays quiet for the rest of that discharge cycle (battery_alert_sent, reset alongside battery_history whenever the existing recharge-jump detection fires) -- not once per wake. New app/mail.py wraps stdlib smtplib (no new dependency); send_email() never raises, so a broken mail server can't 500 a battery report or a password-reset request. Schema migration v2 adds users.email and the two frame columns via ALTER TABLE (safe against the live, already- populated database) plus the two new tables via the existing create_all-based migration runner. Verified against a real (already-migrated, real user/frame data) database: the v1->v2 migration, admin SMTP config + test-email button, full forgot/reset-password roundtrip (including single-use token invalidation and the no-enumeration response), and the battery alert firing exactly once per crossing against a hand-rolled fake SMTP server -- all via curl end-to-end, plus the standing legacy-device curl suite to confirm the device protocol is untouched.
This commit is contained in:
+24
-1
@@ -19,7 +19,7 @@ from sqlalchemy import select, text
|
||||
|
||||
from . import config
|
||||
from .db import SessionLocal, engine
|
||||
from .models import Base, BatteryLog, Frame
|
||||
from .models import Base, BatteryLog, Frame, ServerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,8 +28,20 @@ def _migration_1(conn) -> None:
|
||||
Base.metadata.create_all(bind=conn)
|
||||
|
||||
|
||||
def _migration_2(conn) -> None:
|
||||
"""Adds email (users) and battery-alert threshold (frames) columns,
|
||||
plus the new server_settings/password_reset_tokens tables. ALTER
|
||||
TABLE ADD COLUMN with a default is safe on SQLite against a live,
|
||||
already-populated database -- existing rows just get the default."""
|
||||
conn.execute(text("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''"))
|
||||
conn.execute(text("ALTER TABLE frames ADD COLUMN battery_alert_threshold_pct INTEGER NOT NULL DEFAULT -1"))
|
||||
conn.execute(text("ALTER TABLE frames ADD COLUMN battery_alert_sent INTEGER NOT NULL DEFAULT 0"))
|
||||
Base.metadata.create_all(bind=conn) # creates the two new tables only; existing ones untouched
|
||||
|
||||
|
||||
MIGRATIONS = [
|
||||
(1, _migration_1),
|
||||
(2, _migration_2),
|
||||
]
|
||||
|
||||
|
||||
@@ -50,6 +62,7 @@ def run_migrations() -> None:
|
||||
else:
|
||||
conn.execute(text("UPDATE schema_version SET version = :v"), {"v": version})
|
||||
_ensure_frame_one()
|
||||
_ensure_server_settings()
|
||||
|
||||
|
||||
def new_device_token() -> str:
|
||||
@@ -144,3 +157,13 @@ def _ensure_frame_one() -> None:
|
||||
)
|
||||
else:
|
||||
logger.info("Fresh install: created default frame #%d", frame.id)
|
||||
|
||||
|
||||
def _ensure_server_settings() -> None:
|
||||
"""The SMTP config singleton (id=1) -- created with everything blank
|
||||
(email sending disabled) the first time this runs; /admin edits it in
|
||||
place from then on."""
|
||||
with SessionLocal() as db:
|
||||
if db.get(ServerSettings, 1) is None:
|
||||
db.add(ServerSettings(id=1))
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user