Approve access requests from the dashboard with an emailed temporary password
Each open access request on /dashboard/users gains an Approve form with a suggested username. Approving creates a user-role account with a 20-character random temporary password, emails it to the requester with the sign-in link, and marks the request done; if the email fails the account is deleted so the admin can retry. Approval refuses when mail is not configured. Migration 0008 adds users.must_change_password. A middleware on the signed-in routers sends flagged users to /account?change=1 until they set a new password; login honours the flag regardless of `next`, and the CLI's `users passwd` clears it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
This commit is contained in:
@@ -1 +1 @@
|
||||
{% extends "layout.html" %}{% block ears %}<span>Reader account</span>{% endblock %}{% block content %}<section class="mx-auto mt-12 max-w-md px-4 sm:mt-16 sm:px-6"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Reader account</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">Account</h1>{% if !error.is_empty() %}<p class="error mt-6">{{ error }}</p>{% endif %}<form class="mt-8 grid gap-5 border-b border-rule pb-9 font-sans text-sm" method="post" action="/account/password"><label class="grid gap-1.5 font-medium">Current password <input class="min-h-11 w-full" type="password" name="current_password" autocomplete="current-password" required></label><label class="grid gap-1.5 font-medium">New password <input class="min-h-11 w-full" type="password" name="new_password" autocomplete="new-password" minlength="12" required></label><label class="grid gap-1.5 font-medium">Confirm password <input class="min-h-11 w-full" type="password" name="confirm_password" autocomplete="new-password" minlength="12" required></label><button class="btn-primary mt-1 w-full">Change password</button></form><div class="mt-7 flex flex-wrap gap-3 font-sans text-sm"><form class="m-0" method="post" action="/account/logout-all" data-confirm="Sign out everywhere?"><button class="btn">Sign out everywhere</button></form><form class="m-0" method="post" action="/logout"><button class="btn">Sign out</button></form></div></section>{% endblock %}
|
||||
{% extends "layout.html" %}{% block ears %}<span>Reader account</span>{% endblock %}{% block content %}<section class="mx-auto mt-12 max-w-md px-4 sm:mt-16 sm:px-6"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Reader account</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">Account</h1>{% if change_required %}<p class="notice mt-6">Choose a new password to continue. Your temporary password goes in the current-password field.</p>{% endif %}{% if !error.is_empty() %}<p class="error mt-6">{{ error }}</p>{% endif %}<form class="mt-8 grid gap-5 border-b border-rule pb-9 font-sans text-sm" method="post" action="/account/password"><label class="grid gap-1.5 font-medium">Current password <input class="min-h-11 w-full" type="password" name="current_password" autocomplete="current-password" required></label><label class="grid gap-1.5 font-medium">New password <input class="min-h-11 w-full" type="password" name="new_password" autocomplete="new-password" minlength="12" required></label><label class="grid gap-1.5 font-medium">Confirm password <input class="min-h-11 w-full" type="password" name="confirm_password" autocomplete="new-password" minlength="12" required></label><button class="btn-primary mt-1 w-full">Change password</button></form><div class="mt-7 flex flex-wrap gap-3 font-sans text-sm"><form class="m-0" method="post" action="/account/logout-all" data-confirm="Sign out everywhere?"><button class="btn">Sign out everywhere</button></form><form class="m-0" method="post" action="/logout"><button class="btn">Sign out</button></form></div></section>{% endblock %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h1>Users</h1>
|
||||
<p class="page-desc">{{ requests.len() }} open access request{% if requests.len() != 1 %}s{% endif %}; every account on this server, with its role and open sessions.</p>
|
||||
</div></header>
|
||||
<p class="muted text-sm">Create accounts with <code>daily-epub users add <username></code> on the server, then mark the request done. Change, disable, enable, or sign out users with the <code>daily-epub users</code> CLI.</p>
|
||||
<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>
|
||||
@@ -11,7 +11,7 @@
|
||||
<td class="font-medium text-ink"><a href="mailto:{{ request.email }}">{{ request.email }}</a></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><form method="post" action="/dashboard/users/requests/{{ request.id }}/done"><button class="btn" type="submit">Mark done</button></form></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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user