Files
espresso_frame/server/app/templates/admin.html
T
tfaour c1c803b497
Build and push server image / build-and-push (push) Successful in 38s
SMTP: implicit TLS (port 465) support + fix quarantined mail
Two real fixes to app/mail.py, both found by testing against an actual
mail server rather than just a fake stub:

- Replaces the STARTTLS-only smtp_use_tls boolean with a three-way
  smtp_encryption ("none"/"starttls"/"ssl"). Implicit TLS (port 465,
  what Purelymail and most providers offer alongside 587/STARTTLS) is a
  different handshake entirely -- TLS from the first byte, not a
  plaintext connection that gets upgraded -- so it needs its own
  smtplib.SMTP_SSL code path, not just a skipped starttls() call.
  Schema migration v3 adds the column, backfills it from the old
  boolean, and drops the boolean (safe on a live, populated DB).

- Outgoing mail was missing Date and Message-ID headers -- email.mime
  doesn't set either automatically, and a missing Message-ID in
  particular is enough for a strict content filter (confirmed via a
  real Postfix+Amavis mail server's logs: SPF/DKIM/DMARC all passed
  cleanly, but Amavis quarantined the message as "BAD-HEADER-0" purely
  for the missing id) to silently swallow an otherwise-legitimate
  email, even though smtplib reports success -- the send genuinely
  succeeds to the relay, it just never survives the recipient's own
  filtering. Both headers are now set, with the Message-ID's domain
  matching the From address.

Verified: SMTP_SSL path against a hand-rolled implicit-TLS fake server
(self-signed cert, client-side verification relaxed only in the test
harness -- production code keeps ssl.create_default_context()'s real
verification), the v2->v3 migration against live data, the full admin
SMTP-save + test-email round trip over HTTP, and the standing legacy-
device curl suite.
2026-07-22 01:07:50 -04:00

136 lines
6.9 KiB
HTML

{% extends "app_base.html" %}
{% block title %}Admin{% endblock %}
{% block page_title %}Administration{% endblock %}
{% block content %}
{% if notice %}<div class="status ok">{{ notice }}</div>{% endif %}
{% if error %}<div class="status err">{{ error }}</div>{% endif %}
<div class="layout">
<div class="main-col">
<section class="card">
<h2 class="card-title">Users</h2>
<table class="admin-table">
<thead><tr><th>Username</th><th>Display name</th><th>Role</th><th></th></tr></thead>
<tbody>
{% for u in users %}
<tr>
<td>{{ u.username }}</td>
<td>{{ u.display_name }}</td>
<td>{{ "admin" if u.is_admin else "user" }}</td>
<td class="admin-actions">
<details>
<summary>Reset password</summary>
<form method="post" action="/admin/users/{{ u.id }}/reset-password">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="password" name="password" minlength="8" placeholder="New password" required>
<button type="submit" class="secondary btn-inline">Reset</button>
</form>
</details>
{% if u.id != user.id %}
<form method="post" action="/admin/users/{{ u.id }}/delete"
onsubmit="return confirm('Delete user {{ u.username }}?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="secondary btn-inline">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2 class="card-title" style="margin-top: 24px;">Email (SMTP)</h2>
<p class="sub">Used for "forgot password" links and battery-low
alerts (set per frame in its Configuration tab). Each user needs
an email set in their own Settings for either to reach them.</p>
<form method="post" action="/admin/smtp">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>SMTP server
<input type="text" name="smtp_host" placeholder="smtp.example.com" value="{{ smtp.smtp_host }}">
</label>
<label>Port
<input type="number" name="smtp_port" min="1" max="65535" value="{{ smtp.smtp_port }}">
</label>
<label>Encryption
<select name="smtp_encryption">
<option value="starttls" {% if smtp.smtp_encryption == "starttls" %}selected{% endif %}>STARTTLS (usually port 587)</option>
<option value="ssl" {% if smtp.smtp_encryption == "ssl" %}selected{% endif %}>SSL/TLS (usually port 465)</option>
<option value="none" {% if smtp.smtp_encryption == "none" %}selected{% endif %}>None (usually port 25)</option>
</select>
</label>
<label>Username
<input type="text" name="smtp_username" autocomplete="off" value="{{ smtp.smtp_username }}">
</label>
<label>Password
<input type="password" name="smtp_password" autocomplete="off"
placeholder="{% if smtp.smtp_password %}(unchanged -- enter a new one to replace){% else %}smtp password{% endif %}">
</label>
<label>From address
<input type="text" name="smtp_from_address" placeholder="[email protected]" value="{{ smtp.smtp_from_address }}">
</label>
<button type="submit">Save SMTP settings</button>
</form>
<form method="post" action="/admin/smtp/test" style="margin-top: 8px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="secondary">Send test email to myself</button>
</form>
<h2 class="card-title" style="margin-top: 24px;">Enroll a user</h2>
<form method="post" action="/admin/users">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Username
<input type="text" name="username" maxlength="64" required>
</label>
<label>Password
<input type="password" name="password" minlength="8" required autocomplete="new-password">
</label>
<div class="checkbox-row">
<input type="checkbox" id="is_admin" name="is_admin" value="true">
<label for="is_admin">Administrator</label>
</div>
<button type="submit">Create user</button>
</form>
</section>
</div>
<div class="side-col">
<section class="card">
<h2 class="card-title">Frames</h2>
{% for f in frames %}
<div class="admin-frame">
<p class="sub">
<strong>#{{ f.id }} {{ f.name }}</strong><br>
device: <code>{{ f.device_id or "not yet reported" }}</code><br>
owner: {{ (f.owner.username if f.owner else none) or "UNCLAIMED" }}
&middot; linked: {{ links_by_frame.get(f.id, []) | map(attribute="username") | join(", ") or "nobody" }}<br>
firmware: {{ f.device_firmware_version or "?" }} ({{ f.device_board_variant or "board unknown" }})
&middot; token ack: {{ "yes" if f.device_token_ack else "no" }}
{% if f.legacy_token_enabled %}&middot; <strong>legacy token window OPEN</strong>{% endif %}
</p>
<form method="post" action="/admin/frames/{{ f.id }}/link-user" class="admin-inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="text" name="username" placeholder="Link user by name" required>
<button type="submit" class="secondary btn-inline">Link</button>
</form>
{% if f.legacy_token_enabled %}
<form method="post" action="/admin/frames/{{ f.id }}/end-legacy" class="admin-inline-form"
onsubmit="return confirm('Close the legacy-token window for frame #{{ f.id }}? Only do this once the device has acknowledged its own token.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="secondary btn-inline">Close legacy window</button>
</form>
{% endif %}
<form method="post" action="/admin/frames/{{ f.id }}/delete" class="admin-inline-form"
onsubmit="return confirm('Delete frame #{{ f.id }} and all its history?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="secondary btn-inline">Delete</button>
</form>
</div>
{% endfor %}
{% if not frames %}<p class="sub">No frames yet.</p>{% endif %}
</section>
</div>
</div>
{% endblock %}