Redesign phase B: users, sessions, first-run setup, admin panel
Real identity on top of phase A's schema: scrypt-hashed passwords (stdlib, no new deps -- parameters baked into each stored hash), server-side sessions (sha256 of the cookie value stored, 30-day rolling expiry), and per-session CSRF tokens enforced on every mutating session-authed request -- via X-CSRF-Token for the JSON API (a fetch() wrapper in base.html injects it, so the existing page scripts didn't need touching) and a hidden form field for the HTML forms. /setup runs once while no users exist: creates admin #1, links every existing frame to them (owner + controller), and inherits the migrated Immich creds onto their account -- per-user creds are now the primary source, with env vars still winning as the operator fallback. /login, /logout, /settings (display name, Immich creds, password change), and /admin (enroll users, reset passwords, link users to frames, close a frame's legacy-token window, delete) round out the pages, all in the existing template/card style. The legacy shared token stays accepted on browser routes so the deployed frame's on-panel manage QR keeps working until phase C swaps it for the limited manage page; token access renders without nav or CSRF shim and is exempt from CSRF (explicit credential, not an ambient cookie). Device routes untouched -- the legacy curl suite passes verbatim. Identity is provider-pluggable (identity_provider/provider_subject already modeled) so OIDC can land later without schema surgery.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page_class %}page-narrow{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
<p class="sub">Your account</p>
|
||||
{% 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>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 %}
|
||||
Reference in New Issue
Block a user