Let public visitors request a reader account
New public page GET/POST /request-access explains what a signed-in reader account gets (complete issues online, EPUB/XTC downloads, no ratings or admin tools) and takes an email plus an optional reason. A honeypot field drops bots and a partial unique index keeps one open request per email, updating it on resubmit. Open requests show on /dashboard/users with a "Mark done" button and as a tile on the overview; accounts are still created with the daily-epub users CLI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<div class="tile"><span class="tile-label">Verdicts, 7 days</span><span class="tile-num">{{ ratings_total }}</span><span class="tile-delta"><a href="/dashboard/ratings">rating history</a></span></div>
|
||||
<div class="tile"><span class="tile-label">Unrated picks</span><span class="tile-num">{{ unrated.len() }}</span><span class="tile-delta">from the last three issues</span></div>
|
||||
<div class="tile"><span class="tile-label">Active jobs</span><span class="tile-num">{{ active_jobs.len() }}</span><span class="tile-delta"><a href="/dashboard/jobs">all jobs</a></span></div>
|
||||
<div class="tile"><span class="tile-label">Access requests</span><span class="tile-num">{{ access_requests }}</span><span class="tile-delta"><a href="/dashboard/users">review requests</a></span></div>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
{% extends "layout.html" %}{% block content %}<section class="dashboard">
|
||||
<header class="page-head"><div>
|
||||
<h1>Users</h1>
|
||||
<p class="page-desc">Every account on this server, with its role and open sessions.</p>
|
||||
<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">Accounts are read-only here. Create, change, disable, enable, or sign out users with the <code>daily-epub users</code> CLI on the server.</p>
|
||||
<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>
|
||||
<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>
|
||||
<tbody>{% for request in requests %}<tr>
|
||||
<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>
|
||||
</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>
|
||||
<thead><tr><th>Username</th><th>Role</th><th>Status</th><th>Created</th><th>Last login</th><th class="num">Open sessions</th></tr></thead>
|
||||
<tbody>{% for user in users %}<tr>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<article class="reader-page mx-auto mt-8 max-w-[68ch] px-4 sm:mt-12 sm:px-6">
|
||||
{% if empty %}<div class="py-16 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Morning edition</p><h1 class="mt-3 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">No issue yet</h1><p class="mt-4 text-ink-2">The first issue has not been published.</p></div>{% else %}
|
||||
<header class="mb-10 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ issue.display_date }} · No. {{ issue.issue_number }}</p><p class="mt-2 font-sans text-sm text-muted">{{ issue.stats_line }}</p><p class="mx-auto mt-5 max-w-[58ch] text-lg italic leading-relaxed text-ink-2">A personal morning paper, assembled daily; the selection is the reader's, the words are the authors'.</p></header>
|
||||
<p class="notice mb-8"><a href="/login">Sign in</a> to read every article's full text and download the editions, or <a href="/request-access">request access</a>.</p>
|
||||
{% if !downloads.is_empty() %}<div class="mb-8 flex flex-wrap justify-center gap-2">{% for download in downloads %}<a class="btn" href="{{ download.href }}">{{ download.label }} <span class="ml-1 text-muted">{{ download.size }}</span></a>{% endfor %}</div>{% endif %}
|
||||
{% for section in issue.sections %}<section class="mt-14"><h2 class="reader-section-heading">{{ section.name }}</h2><div>{% for entry in section.entries %}<article class="border-b border-rule py-6{% if entry.is_lead %} pt-5{% endif %}"><h3 class="font-semibold leading-[1.1] tracking-[-0.01em] {% if entry.is_lead %}text-3xl{% else %}text-2xl{% endif %}"><a class="text-ink no-underline hover:text-accent" href="{{ entry.url }}">{{ entry.title }}</a></h3><p class="mt-2 font-sans text-sm leading-relaxed text-muted">{% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{{ entry.source }}{% if !entry.domain.is_empty() %} ({{ entry.domain }}){% endif %} · {{ entry.reading_minutes }} min</p>{% match entry.summary %}{% when Some with (summary) %}<p class="index-summary mt-4">{{ summary }}</p>{% when None %}{% endmatch %}{% match entry.why %}{% when Some with (why) %}<p class="mt-4 border-l-2 border-accent pl-3 italic text-ink-2">Why it's here: {{ why }}</p>{% when None %}{% endmatch %}{% if !entry.comment_links.is_empty() %}<p class="mt-3 flex flex-wrap gap-x-4 gap-y-1 font-sans text-sm text-muted">{% for link in entry.comment_links %}<a class="text-muted hover:text-accent" rel="noopener" target="_blank" href="{{ link.url }}">{{ link.label }}{% if !link.meta.is_empty() %}: {{ link.meta }}{% endif %}</a>{% endfor %}</p>{% endif %}</article>{% endfor %}</div></section>{% endfor %}
|
||||
<p class="mt-10 text-center font-sans text-sm"><a href="/issues">Browse the archive →</a></p>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{% extends "layout.html" %}{% block ears %}<span>Reader access</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 access</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">Sign in</h1><p class="mt-4 text-ink-2">Open the full paper, article chapters, and downloads.</p>{% if !error.is_empty() %}<p class="error mt-6">{{ error }}</p>{% endif %}<form class="mt-8 grid gap-5 font-sans text-sm" method="post" action="/login"><input type="hidden" name="next" value="{{ next }}"><label class="grid gap-1.5 font-medium">Username <input class="min-h-11 w-full" name="username" autocomplete="username" required></label><label class="grid gap-1.5 font-medium">Password <input class="min-h-11 w-full" type="password" name="password" autocomplete="current-password" required></label><button class="btn-primary mt-1 w-full" type="submit">Sign in</button></form></section>{% endblock %}
|
||||
{% extends "layout.html" %}{% block ears %}<span>Reader access</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 access</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">Sign in</h1><p class="mt-4 text-ink-2">Open the full paper, article chapters, and downloads.</p>{% if !error.is_empty() %}<p class="error mt-6">{{ error }}</p>{% endif %}<form class="mt-8 grid gap-5 font-sans text-sm" method="post" action="/login"><input type="hidden" name="next" value="{{ next }}"><label class="grid gap-1.5 font-medium">Username <input class="min-h-11 w-full" name="username" autocomplete="username" required></label><label class="grid gap-1.5 font-medium">Password <input class="min-h-11 w-full" type="password" name="password" autocomplete="current-password" required></label><button class="btn-primary mt-1 w-full" type="submit">Sign in</button></form><p class="mt-6 text-center font-sans text-sm text-muted">Don't have an account? <a href="/request-access">Request access</a>.</p></section>{% endblock %}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "layout.html" %}{% block ears %}<span>Reader access</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 access</p><h1 class="mt-2 text-4xl font-semibold leading-[1.1] tracking-[-0.01em]">Request access</h1>{% if submitted %}<p class="notice mt-6">Thanks — requests are reviewed by hand; you'll hear back by email.</p>{% else %}<p class="mt-4 text-ink-2">Signed-in accounts can read the complete issue online, including every article's full text, and download the EPUB and XTC editions. They have no access to ratings or admin tools.</p>{% if !error.is_empty() %}<p class="error mt-6">{{ error }}</p>{% endif %}<form class="mt-8 grid gap-5 font-sans text-sm" method="post" action="/request-access"><label class="grid gap-1.5 font-medium">Email <input class="min-h-11 w-full" type="email" name="email" autocomplete="email" value="{{ email }}" maxlength="254" required></label><label class="grid gap-1.5 font-medium">Reason or comment <span class="font-normal text-muted">Optional</span><textarea class="w-full" name="reason" maxlength="2000">{{ reason }}</textarea></label><label class="sr-only" aria-hidden="true">Website <input name="website" autocomplete="off" tabindex="-1"></label><button class="btn-primary mt-1 w-full" type="submit">Request access</button></form>{% endif %}</section>{% endblock %}
|
||||
Reference in New Issue
Block a user