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.
49 lines
2.1 KiB
HTML
49 lines
2.1 KiB
HTML
{% extends "app_base.html" %}
|
|
|
|
{% block title %}Settings{% endblock %}
|
|
{% block page_title %}Your account{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if saved %}<div class="status ok">Saved.</div>{% endif %}
|
|
{% if error %}<div class="status err">{{ error }}</div>{% endif %}
|
|
|
|
<section class="card">
|
|
<h2 class="card-title">Profile & photo library</h2>
|
|
<form method="post" action="/settings">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<label>Display name
|
|
<input type="text" name="display_name" maxlength="64" value="{{ user.display_name }}">
|
|
</label>
|
|
<label>Email
|
|
<input type="email" name="email" value="{{ user.email }}" placeholder="[email protected]">
|
|
</label>
|
|
<p class="sub" style="margin-top: 8px;">Used for password-reset links
|
|
and, for frames you own, battery-low alerts (set a threshold in a
|
|
frame's Configuration tab).</p>
|
|
<label>Immich URL
|
|
<input type="text" name="immich_url" placeholder="http://your-immich-host:2283"
|
|
value="{{ user.immich_url }}">
|
|
</label>
|
|
<label>Immich API key
|
|
<input type="password" name="immich_api_key" autocomplete="off"
|
|
placeholder="{% if user.immich_api_key %}(unchanged -- enter a new key to replace){% else %}your-immich-api-key{% endif %}">
|
|
</label>
|
|
<p class="sub" style="margin-top: 8px;">Frames you own pull photos from
|
|
this Immich library. The key needs read access to albums/assets/faces
|
|
plus <code>sharedLink.create</code> for the on-frame share QR.</p>
|
|
|
|
<h2 class="card-title" style="margin-top: 24px;">Change password</h2>
|
|
<label>Current password
|
|
<input type="password" name="current_password" autocomplete="current-password">
|
|
</label>
|
|
<label>New password
|
|
<input type="password" name="new_password" minlength="8" autocomplete="new-password">
|
|
</label>
|
|
<p class="sub" style="margin-top: 8px;">Leave both blank to keep your
|
|
current password.</p>
|
|
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
</section>
|
|
{% endblock %}
|