Skip to main content

max / makenotwork

Describe the four sessionless auth pages, so the caret has somewhere to start `/login`, `/forgot-password`, `/reset-password` and `/auth/2fa` are described screens now, and the first consumer of `Screen::opens_at`. Each carried exactly one `autofocus`, and the caret landing in the one box a reader came here to type into is the whole of what these pages do. Five templates and five Askama structs are gone: the four pages plus `partials/login_error.html`. Not on a quasi mount, deliberately. `Viewer` carries the app state, the runtime and the token and does not carry the session, and all four of these GETs need it: `/auth/2fa` reads a pending-2FA key and re-checks the tracking row, `/reset-password` re-validates a signed HMAC link, `/login` reads two query parameters and `Config::sso`. So the routes keep the extractors they had and what is described is the document. Moving the addresses onto the mount would be a rewrite of four auth guards to change no pixel. The POST handlers answer into the region the description names, which is what `Action::replacing` is documented for. Two things followed from that: - A form's answer replaces the region itself (`hx-swap="outerMorph"`), so a bare alert would replace the element the next attempt has to aim at and a second wrong password would land nowhere. `auth_pages::answered` renders the region with its id, and the four handlers answer with that instead of `LoginErrorTemplate`/`AlertTemplate`. The banner is the description's now, drawn by the renderer that drew the page. - `/auth/verify-2fa` targeted `closest .login-container` with `outerHTML` and answered with a whole rendered page, so a mistyped digit swapped an entire document into a `div`. It answers into `form-feedback` like its siblings. Two things the vocabulary could not say, handled rather than dropped: - The passkey offer is a `RegionKind::Handover`. A `data-action` the classic dispatcher resolves is not something a description should learn to say, so the description says there is a place here and the host pays the markup. `page-login-2.js` states WebAuthn support as `data-supported` rather than taking a class off markup it no longer owns. - `reset_password.html` wrote `minlength="8"`. `Field` has a maximum and no minimum, and adding one means a member on the published `makeover_layout::Field` and a cascade through the whole suite for a browser hint. The rule is the server's and always was, so the field carries a hint in words instead -- said before the reader submits rather than after. Also fixes five clippy findings this pass did not introduce, in `collections`, `creators` and the two git annotation modules: a clippy bump had left the gate red for everyone.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-02 13:25 UTC
Signed with PGP, not checked
Commit: f4debfecbf5897eca54215eab8c0b55fa1011a1c
Parent: 257af74
21 files changed, +766 insertions, -433 deletions
@@ -1,5 +1,10 @@
1 - // Show passkey button if browser supports WebAuthn (absent in SSO-only mode)
1 + // Offer the passkey button only where the browser can do WebAuthn.
2 + //
3 + // The container is a described `RegionKind::Handover` (`crate::quasi::auth_pages`),
4 + // so its markup is the renderer's and this script cannot ship a class on it.
5 + // It states the answer as a data attribute instead and `style.css` keys the
6 + // region off that, which is the same arrangement the header's cart place uses.
2 7 var passkeyLogin = document.getElementById('passkey-login');
3 8 if (window.PublicKeyCredential && passkeyLogin) {
4 - passkeyLogin.classList.remove('hidden');
9 + passkeyLogin.dataset.supported = 'true';
5 10 }
@@ -4391,6 +4391,25 @@
4391 4391 border-bottom: 2px solid var(--warning);
4392 4392 }
4393 4393
4394 + /* A described `Node::Notice` carries its tone as an attribute rather than in
4395 + the class, which is what `quasi-webview` emits and what `makeover` keys every
4396 + other tone off. The two `banner--` modifiers above are this site's own
4397 + full-bleed notices and are untouched. */
4398 + .banner[data-tone="danger"] {
4399 + background: color-mix(in oklch, var(--danger) 14%, var(--surface-page));
4400 + border-bottom: 2px solid var(--danger);
4401 + }
4402 +
4403 + .banner[data-tone="success"] {
4404 + background: color-mix(in oklch, var(--success) 14%, var(--surface-page));
4405 + border-bottom: 2px solid var(--success);
4406 + }
4407 +
4408 + .banner[data-tone="info"] {
4409 + background: var(--highlight-faint);
4410 + border-bottom: 2px solid var(--focus-ring);
4411 + }
4412 +
4394 4413 .banner a {
4395 4414 color: inherit;
4396 4415 text-decoration: underline;
@@ -4716,7 +4735,12 @@
4716 4735 SPECIFIC COMPONENTS
4717 4736 =========================================== */
4718 4737
4719 - /* Login container */
4738 + /* Login container.
4739 + The id as well as the class: the four auth pages are described screens now
4740 + (`crate::quasi::auth_pages`) and a description names a region, never a class,
4741 + so the region's id is what a rule can key off. The class stays for the six
4742 + templated pages that still write it. */
4743 + #login-container,
4720 4744 .login-container {
4721 4745 display: flex;
4722 4746 flex-direction: column;
@@ -4764,6 +4788,15 @@
4764 4788 margin-top: 0.5em;
4765 4789 }
4766 4790
4791 + /* The described auth pages have no `.foot-link` wrapper: a description says
4792 + there is a link, and a `div` around it to make it a block is markup with no
4793 + meaning in it. Stacking them is this stylesheet's answer, which is where a
4794 + layout question a description declines belongs. */
4795 + #login-container > .link {
4796 + display: block;
4797 + margin-top: 0.5em;
4798 + }
4799 +
4767 4800 /* Status indicators */
4768 4801 .status-indicator {
4769 4802 display: inline-block;
@@ -7373,6 +7406,7 @@
7373 7406 font-size: var(--text-hero);
7374 7407 }
7375 7408
7409 + #login-container,
7376 7410 .login-container,
7377 7411 .form-container {
7378 7412 max-width: 100%;
@@ -10816,6 +10850,12 @@
10816 10850 opacity: 0.5;
10817 10851 font-size: var(--text-note);
10818 10852 }
10853 + /* The passkey offer is out only where the browser can do WebAuthn.
10854 + `page-login-2.js` asks and states the answer; the region's own markup is the
10855 + renderer's, so there is no class for a script to take off. */
10856 + #passkey-login { display: none; }
10857 + #passkey-login[data-supported] { display: block; }
10858 +
10819 10859 .login-passkey-btn { width: 100%; }
10820 10860 .login-passkey-error { margin-top: var(--gap-peer); }
10821 10861
@@ -63,6 +63,15 @@
63 63 }
64 64
65 65 /// One item in the collection.
66 + ///
67 + /// `item_type` reads as a repetition of the struct's name and is not one: it is
68 + /// the item's *kind* -- track, video, document -- and `type` is a keyword.
69 + /// Named here rather than allowed at the crate root, so the exception is beside
70 + /// the thing it excuses.
71 + #[allow(
72 + clippy::struct_field_names,
73 + reason = "`type` is a keyword; this is the kind"
74 + )]
66 75 struct Item {
67 76 id: String,
68 77 title: String,
@@ -281,11 +281,13 @@
281 281 /// rather than written.
282 282 #[test]
283 283 fn every_tier_is_priced_from_the_live_figures() {
284 - let mut prices = TierPrices::default();
285 - prices.basic_std = 4321;
286 - prices.small_files_std = 5678;
287 - prices.big_files_std = 8765;
288 - prices.everything_std = 9876;
284 + let prices = TierPrices {
285 + basic_std: 4321,
286 + small_files_std: 5678,
287 + big_files_std: 8765,
288 + everything_std: 9876,
289 + ..TierPrices::default()
290 + };
289 291
290 292 let html = {
291 293 use quasi_axum::Serves as _;
@@ -37,6 +37,7 @@
37 37
38 38 use crate::AppState;
39 39 use crate::auth::SessionUser;
40 + pub mod auth_pages;
40 41
41 42 pub mod buyer_contacts;
42 43 pub mod cart_act;
@@ -27,7 +27,7 @@
27 27 email,
28 28 error::{AppError, Result, ResultExt},
29 29 helpers::{is_htmx_request, rate_limiter_ms, rate_limiter_per_sec},
30 - templates::{LoginErrorTemplate, LoginTemplate, SaveStatusTemplate, UsernameStatusTemplate},
30 + templates::{SaveStatusTemplate, UsernameStatusTemplate},
31 31 };
32 32 use sqlx::PgPool;
33 33 use webauthn_rs::prelude::*;
@@ -125,24 +125,24 @@
125 125 let return_error = |msg: &str| -> Result<Response> {
126 126 crate::security_signals::note_auth_failure(failure_ip.as_deref());
127 127 if is_htmx {
128 - Ok(Html(
129 - LoginErrorTemplate {
130 - message: msg.to_string(),
131 - }
132 - .render_string()?,
133 - )
128 + // The region the described form aims at, with its id: see
129 + // `auth_pages::answered`. A bare alert would replace the element a
130 + // second attempt has to land in.
131 + Ok(Html(crate::quasi::auth_pages::answered(
132 + crate::quasi::auth_pages::LOGIN_FEEDBACK,
133 + makeover_layout::Tone::Danger,
134 + msg,
135 + None,
136 + ))
134 137 .into_response())
135 138 } else {
136 139 // Full-page POST: re-render the login form with the username/email
137 140 // value preserved and the error inlined, instead of bouncing the
138 141 // user to the global error page (which loses every field).
139 - Ok(LoginTemplate {
140 - csrf_token: recall_csrf_token.clone(),
141 - prefill_login: submitted_login.clone(),
142 - error: Some(msg.to_string()),
143 - notice: None,
144 - sso_enabled,
145 - }
142 + Ok(axum::response::Html(crate::quasi::auth_pages::document(
143 + recall_csrf_token.as_deref(),
144 + &crate::quasi::auth_pages::login(&submitted_login, Some(msg), None, sso_enabled),
145 + ))
146 146 .into_response())
147 147 }
148 148 };
@@ -144,11 +144,7 @@
144 144 IndexTemplate,
145 145 LibraryTemplate,
146 146 CartTemplate,
147 - LoginTemplate,
148 - TwoFactorTemplate,
149 147 OAuthAuthorizeTemplate,
150 - ForgotPasswordTemplate,
151 - ResetPasswordTemplate,
152 148 UserTemplate,
153 149 ProjectTemplate,
154 150 ProjectPaywallTemplate,
@@ -118,19 +118,6 @@
118 118 pub download_url: String,
119 119 }
120 120
121 - /// HTMX partial: inline login error message.
122 - #[derive(Template)]
123 - #[template(path = "partials/login_error.html")]
124 - pub struct LoginErrorTemplate {
125 - pub message: String,
126 - }
127 -
128 - impl LoginErrorTemplate {
129 - pub fn render_string(&self) -> crate::error::Result<String> {
130 - crate::helpers::render_fragment(self)
131 - }
132 - }
133 -
134 121 /// HTMX partial: username availability check result.
135 122 #[derive(Template)]
136 123 #[template(path = "partials/username_status.html")]