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:
2026-09-06 18:02:18 +00:00
co-authored by Claude Fable 5.1
parent 142a8d9905
commit 87ff3d92c8
12 changed files with 445 additions and 12 deletions
+7
View File
@@ -421,6 +421,7 @@ struct OverviewTemplate {
budget: Vec<BudgetLine>,
ratings: Vec<LabelCount>,
ratings_total: i64,
access_requests: i64,
unrated: Vec<UnratedPick>,
active_jobs: Vec<JobLine>,
finished_jobs: Vec<JobLine>,
@@ -442,6 +443,11 @@ async fn overview(
let last_run = last_run_card(db, &config).await?;
let budget = budget_lines(db, &config, now).await?;
let (ratings, ratings_total) = ratings_this_week(db, now).await?;
let access_requests: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM account_requests WHERE status = 'open'")
.fetch_one(db.pool())
.await
.map_err(db_err)?;
let unrated = unrated_picks(db).await?;
let (active_jobs, finished_jobs) = jobs_summary(db, &config).await?;
let sparklines = overview_sparklines(db).await?;
@@ -459,6 +465,7 @@ async fn overview(
budget,
ratings,
ratings_total,
access_requests,
unrated,
active_jobs,
finished_jobs,