Public: anyone can read this; the crew can change it.
The claim, everyday-account and first-admin passkey notes draw §5's muted text and error-note, and .passkey-note leaves their markup #4376
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4370-the-claim-everyday-account-and-first-adm → main 3 commits fork
7 files
changed +13
−5
src/web/pages/claim.ts +1 −1
| @@ -198,7 +198,7 @@ function claimForm( | ||
| 198 | 198 | Or skip the password: a passkey signs you in with a fingerprint, |
| 199 | 199 | face or device PIN. It needs an email address above. |
| 200 | 200 | </p> |
| 201 | <p class="passkey-note mb" id="passkey-make-note" role="alert">${errors.passkey ?? ""}</p> | |
| 201 | <p class="${errors.passkey ? "error-note" : "muted small"} mb" id="passkey-make-note" data-fail="error-note" role="${errors.passkey ? "alert" : "status"}">${errors.passkey ?? ""}</p> | |
| 202 | 202 | <button type="button" id="passkey-make" class="btn" data-variant="outline" data-options="/claim/options" data-needs-email>Claim with a passkey instead</button> |
| 203 | 203 | </div> |
| 204 | 204 | </form>`, |
src/web/pages/everyday.ts +1 −1
| @@ -108,7 +108,7 @@ function everydayFields(ctx: Ctx, user: User, offerKeep: boolean): Html { | ||
| 108 | 108 | It signs in with a passkey of its own: a fingerprint, face or |
| 109 | 109 | device PIN. There is no password. |
| 110 | 110 | </p> |
| 111 | <p class="passkey-note" id="passkey-make-note" role="alert"></p> | |
| 111 | <p class="muted small" id="passkey-make-note" data-fail="error-note" role="status"></p> | |
| 112 | 112 | <noscript><p class="muted small">Making the passkey needs JavaScript.</p></noscript> |
| 113 | 113 | <div class="form-actions"> |
| 114 | 114 | <button type="submit" id="passkey-make" class="btn" data-variant="primary" data-options="/settings/everyday/options">Make it with a passkey</button> |
src/web/pages/first-admin.ts +1 −1
| @@ -78,7 +78,7 @@ function form(values: Values, errors: Errors): Html { | ||
| 78 | 78 | You sign in with a passkey: a fingerprint, face or device PIN. |
| 79 | 79 | There is no password to remember. |
| 80 | 80 | </p> |
| 81 | <p class="passkey-note mb" id="passkey-make-note" role="alert">${errors.passkey ?? ""}</p> | |
| 81 | <p class="${errors.passkey ? "error-note" : "muted small"} mb" id="passkey-make-note" data-fail="error-note" role="${errors.passkey ? "alert" : "status"}">${errors.passkey ?? ""}</p> | |
| 82 | 82 | <noscript><p class="muted small">Making the passkey needs JavaScript.</p></noscript> |
| 83 | 83 | <div class="form-actions"> |
| 84 | 84 | <button type="submit" id="passkey-make" class="btn" data-variant="primary" data-options="/first-admin/options">Make the admin with a passkey</button> |
test/claim-link.test.ts +4 −1
| @@ -212,7 +212,10 @@ describe("the claim link in the fragment, and its re-issue", () => { | ||
| 212 | 212 | ...(await passkeyFields(childServers.url, "/claim/options", takenForm, { device: shared })), |
| 213 | 213 | }); |
| 214 | 214 | expect(taken.status).toBe(422); |
| 215 | expect(await taken.text()).toContain("That passkey is already registered."); | |
| 215 | const takenPage = await taken.text(); | |
| 216 | expect(takenPage).toContain("That passkey is already registered."); | |
| 217 | expect(takenPage).not.toContain("passkey-note"); | |
| 218 | expect(takenPage).toMatch(/<p class="error-note mb" id="passkey-make-note" data-fail="error-note" role="alert">That passkey is already registered\./); | |
| 216 | 219 | expect(getUserByName(child.db, "taker")).toBeUndefined(); |
| 217 | 220 | expect(listPasskeys(child.db, holder.value.id)).toHaveLength(1); |
| 218 | 221 | // Still unspent: the link gives a challenge yet. |
test/everyday.test.ts +2
| @@ -182,6 +182,8 @@ describe("the everyday account", () => { | ||
| 182 | 182 | const root = await signIn("ada"); |
| 183 | 183 | const home = await page(root, "/"); |
| 184 | 184 | expect(home).toContain('id="passkey-make"'); |
| 185 | expect(home).not.toContain("passkey-note"); | |
| 186 | expect(home).toContain('<p class="muted small" id="passkey-make-note" data-fail="error-note" role="status"></p>'); | |
| 185 | 187 | expect(home).not.toContain('name="password"'); |
| 186 | 188 | const res = await make(root, { |
| 187 | 189 | username: "ada-me", |
test/first-admin.test.ts +3
| @@ -298,6 +298,9 @@ describe("a person: /first-admin with a passkey", () => { | ||
| 298 | 298 | redirect: "manual", |
| 299 | 299 | }); |
| 300 | 300 | expect(res.status).toBe(422); |
| 301 | const refusedPage = await res.text(); | |
| 302 | expect(refusedPage).not.toContain("passkey-note"); | |
| 303 | expect(refusedPage).toMatch(/<p class="error-note mb" id="passkey-make-note" data-fail="error-note" role="alert">[^<]+<\/p>/); | |
| 301 | 304 | expect(getUserByName(app.db, "ada")).toBeUndefined(); |
| 302 | 305 | expect(readFirstAdmin(app.db, code)).toBe("ok"); |
| 303 | 306 | }); |
test/web.test.ts +1 −1
| @@ -459,7 +459,7 @@ describe("web pages end to end", () => { | ||
| 459 | 459 | expect(changeCapacity(app, ada, "cells.egress", "report").ok).toBe(true); |
| 460 | 460 | try { |
| 461 | 461 | const text = await (await fetch(base, { headers: { cookie } })).text(); |
| 462 | expect(text).not.toContain("error-note"); | |
| 462 | expect(text).not.toMatch(/class="[^"]*\berror-note/); | |
| 463 | 463 | expect(text).not.toContain("Egress is reported"); |
| 464 | 464 | } finally { |
| 465 | 465 | changeCapacity(app, ada, "cells.egress", "enforce"); |
new above
offline