max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
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")] |