Carry next through the password change and let requesters pick a username

A forced or voluntary password change now redirects to the validated
`next` target (the page the user was heading for, or the home page)
instead of landing back on the account form. Login and the
must-change-password middleware pass the destination along as
/account?change=1&next=... and the form carries it as a hidden field.

The request-access form gains a required Username field validated with
the account rules and rejected when an account or another open request
already holds it (case-insensitive). Migration 0009 stores it on the
request; the notification email and the Users dashboard show it, and
the Approve form is prefilled with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
This commit is contained in:
2026-09-07 02:28:09 +00:00
co-authored by Claude Fable 5.1
parent a3a041dc06
commit e56acfb9dc
8 changed files with 301 additions and 37 deletions
+3 -2
View File
@@ -6,12 +6,13 @@
<p class="muted text-sm">Approve creates a user account with a temporary password and emails it; the user must choose a new password on first sign-in. Or create accounts with <code>daily-epub users add</code> and mark the request done. Change, disable, enable, or sign out users with the <code>daily-epub users</code> CLI.</p>
<section class="card"><h2>Access requests</h2>
{% if requests.is_empty() %}<p class="muted">No open requests.</p>{% else %}<div class="scroll-x"><table>
<thead><tr><th>Email</th><th>Reason or comment</th><th>Requested</th><th><span class="sr-only">Action</span></th></tr></thead>
<thead><tr><th>Email</th><th>Username</th><th>Reason or comment</th><th>Requested</th><th><span class="sr-only">Action</span></th></tr></thead>
<tbody>{% for request in requests %}<tr>
<td class="font-medium text-ink"><a href="mailto:{{ request.email }}">{{ request.email }}</a></td>
<td class="font-medium text-ink">{{ request.suggested_username }}</td>
<td class="cell-wrap">{% if request.reason.is_empty() %}<span class="text-muted">—</span>{% else %}{{ request.reason }}{% endif %}</td>
<td class="cell-tight text-muted">{{ request.requested }}</td>
<td><div class="flex flex-wrap gap-2"><form class="form-inline" method="post" action="/dashboard/users/requests/{{ request.id }}/approve"><input type="text" name="username" value="{{ request.suggested_username }}" required pattern="[a-z0-9_-]+" aria-label="Username for {{ request.email }}"><button class="btn-primary" type="submit"{% if !mail_configured %} disabled title="Email is not configured"{% endif %}>Approve</button></form><form method="post" action="/dashboard/users/requests/{{ request.id }}/done"><button class="btn" type="submit">Mark done</button></form></div></td>
<td><div class="flex flex-wrap gap-2"><form class="form-inline" method="post" action="/dashboard/users/requests/{{ request.id }}/approve"><input type="text" name="username" value="{{ request.suggested_username }}" autocomplete="username" maxlength="32" required pattern="[A-Za-z0-9._-]+" aria-label="Username for {{ request.email }}"><button class="btn-primary" type="submit"{% if !mail_configured %} disabled title="Email is not configured"{% endif %}>Approve</button></form><form method="post" action="/dashboard/users/requests/{{ request.id }}/done"><button class="btn" type="submit">Mark done</button></form></div></td>
</tr>{% endfor %}</tbody></table></div>{% endif %}
</section>
{% if users.len() > 1 %}<input type="search" class="table-filter" placeholder="Filter rows on this page" aria-label="Filter rows on this page" data-table-filter>{% endif %}<div class="scroll-x tall"><table data-filter>