Skip to content

The auth screens say what happens next, in the buyer's words - #337

Merged
Apolloccrypt merged 3 commits into
mainfrom
feat/fe-auth
Sep 2, 2026
Merged

The auth screens say what happens next, in the buyer's words#337
Apolloccrypt merged 3 commits into
mainfrom
feat/fe-auth

Conversation

@Apolloccrypt

@Apolloccrypt Apolloccrypt commented Sep 2, 2026

Copy link
Copy Markdown
Owner

The five auth screens plus /signup/verified, rewritten so each one says what it is for and what happens after the button. Rebased on main, and two files that did not belong here are back at their main version.

Scope was cut back

/account and /js/account.inline1.js are out of this PR. #332 (feat/dashboard-ingelogd) rewrites both: plan name Community, the paid band per product, the Cancel gate on product plans. Two PRs editing the same two files is a merge conflict waiting to happen and the dashboard PR owns the account screen, so this branch leaves them at main. The account assertions came out of tests/ui-truthfulness.test.mjs with them, including the one that would have forced sessions and your authenticator app link are removed on a file this branch no longer touches.

frontend/js/account.inline2.js and frontend/js/passkey.js are back at main for the same reason: their changes were account-screen copy. /js/passkey.js therefore goes back to ?v=3 on login and setup.

/download is out of this PR, back at the main version. It is not an auth screen, and it was rejected twice on its own merits: internally contradictory (a banner telling you not to install it above a full product page), ML-KEM-768 and AES-256-GCM in the first screenful, 37 bits of nickname entropy, and a signing claim about the AppImage that nothing in the repo evidences. None of that is fixed by editing it inside an auth PR.

Backlog item, not carried here: /download needs its own PR. One answer per page (use the web app), the desktop build and its 21 protections behind a disclosure, the installer signing status stated only as far as the repo can prove it, and a real button in the first screenful instead of a domain name buried in a warning paragraph.

What changed

frontend/apply-nav.py was run after the rebase. It produced no diff: the nav and legal strip on these pages already match main.

Layout, measured at 390px with Playwright

/auth/request-reset and /auth/reset-confirm used <main class="page-main">, and no stylesheet in frontend/ defines that class. The heading and the primary button sat flush against both screen edges. Both now use container-sm, the container /auth/backup already uses.

.lede carries no top margin and h1 no bottom margin, so on setup, backup and request-reset the heading touched the lead paragraph. Those three take the existing .mt-3 utility (16px), which leaves .lede alone for the eleven other pages that use it.

screen h1 left/right before after h1 to lede before after h-overflow
/auth/login 21 / 21 px unchanged 8 px unchanged 0
/auth/setup 24 / 24 px unchanged 0 px 16 px 0
/auth/backup 24 / 24 px unchanged 0 px 16 px 0
/auth/request-reset 0 / 0 px 24 / 24 px 0 px 16 px 0
/auth/reset-confirm 0 / 0 px 24 / 24 px 36 px unchanged 0
/signup/verified 20 / 20 px unchanged 16 px unchanged 0

The YES, RESET MY AUTHENTICATOR button on reset-confirm ran to the left edge; it now starts at 24px. No page scrolls horizontally at 390px.

Text that did not match the code

The reset confirmation link lasts 60 minutes, not 15. The token is written with { EX: 3600 } at admin/server.js:1890 (// 60 minutes), and the admin-initiated variant at :3173 uses the same TTL. There is no 15 minute TTL anywhere. The number was already wrong on main, but the old draft rewrote that very sentence and attached a safety justification to it, which made a wrong number sound deliberate.

No delivery promise on either screen that made one. Request a new one and it arrives within a minute is gone from reset-confirm: sendResetConfirmEmail is fire-and-forget inside a try/catch (admin/server.js:1692) and nothing measures delivery. The same unsourced promise on /signup/verified (Delivery can take up to 60 seconds) goes with it. The earlier draft applied this rule to one screen and not the other, which is not a rule.

reset-confirm now says why the reset takes two mails. You have just clicked a link in an email and the page tells you a second email follows. Without a reason that reads as phishing. The page now says the split is the point: the second link is only sent after this confirmation and has to be opened as well, so someone who briefly had your mailbox open cannot finish the reset.

The 429 on sign-in no longer blames the device. The limit is double, admin/server.js:974-979: per IP (5 per 900s) and per email address (10 per 900s), and ipCount > 5 || emailCount > 10 returns the 429. The email counter is account-wide, so another device can cause it, and a shared connection can cause it without this account being involved. The text now names both counters instead of pointing the reader at the wrong cause.

The first reset mail is a confirmation, not the setup link. auth-request-reset.js told you a setup link is on its way ... The link works for 14 days. The handler does something else: it writes a confirmation token with { EX: 3600 } (admin/server.js:1890) and mails that. The 14 day setup token is only written after the confirmation is opened, in the reset-confirm handler (admin/server.js:1935), so at the moment that message appears no setup link exists yet. The success message now names both mails and puts each lifetime on the right one: 60 minutes for the confirmation, 14 days for the setup link that follows. /auth/reset-confirm was already saying this correctly, so the two screens now agree. The lede had the same fold and got the same correction.

The success message could not be seen when it fired. #success sat inside #reset-form, and the handler hides that form on success, so the confirmation was hidden along with its parent and the screen went blank after submitting. It now sits after the form. This was already the case on main and surfaced while rendering the new text; none of the other auth screens nest a result inside an element they hide.

429 on request-reset got its own branch. It fell through to Try again, while the server answers retry_after: 86400 off a limit of 5 per address per 24 hours and 10 per connection per hour (admin/server.js:1875). The text now says the wait can run to 24 hours, offers the support address, and the button stays disabled, because retrying does not help.

/signup/verified stopped asking a lawyer to rank authenticator apps. A SHA-256 app is stronger: Raivo (iOS, open source), Aegis (Android, open source), 1Password, Bitwarden, Authy, Ente Auth or 2FAS appeared twice. It now names Google Authenticator and Microsoft Authenticator, says a password manager you already use also works, and links /help/authenticator-apps for the comparison. The orphan step badge 2 on a page with no step 1 is now a phrase.

Tests

tests/ui-truthfulness.test.mjs pins every one of the above. Twelve sabotages in the working tree, each reverted, all twelve red:

sabotage result
backup lede loses mt-3 red
reset-confirm TTL back to 15 minutes red
429 on login back to a device claim red
request-reset back to page-main red
verified re-adds the 60-second promise red
reset-confirm drops the two-mail reason red
request-reset: 60 minutes to 99 minutes red
request-reset: setup-link wording back red
request-reset: 14 days detached from the second mail red
request-reset: 429 branch removed red
request-reset: the 24 hours softened red
request-reset: #success back inside the form red

The page-main assertion is structural rather than textual: <main> on those two pages must carry a class that some stylesheet actually defines, which is the defect that produced the 0px margin in the first place.

Green locally: links, seo-contract, ui-truthfulness, frontend-loading-contract, navigation-shell (25 checks), csp-inline, cache-bust (335 links, one consistent ?v=), eslint, and tests/static-sanity.sh PASS with all 10 sections green.

frontend/index.html and frontend/js/nav-auth.js are not in the diff. frontend/apply-nav.py is not in the diff either: running it changed nothing.

Apolloccrypt added a commit that referenced this pull request Sep 2, 2026
Review of PR #337 found sixteen defects. This is that list, and only that list.

Layout. /auth/request-reset and /auth/reset-confirm used <main class="page-main">,
a class no stylesheet in frontend/ defines, so at 390px the heading and the blue
button ran flush to both edges. Both now use container-sm plus section-lg/section,
the shape /auth/backup already had. Measured at 390px: 24px each side, matching
backup (24) and login (21).

Truth. Four claims did not survive a check against admin/server.js:

- the reset confirmation token is set with { EX: 3600 }, not 15 minutes;
- "arrives within a minute" was a delivery promise nothing in the code makes,
  and the mail is sent fire-and-forget;
- the sign-in 429 is per IP (5/900s) AND per email address (10/900s), so another
  device can cause it and "from this device" sent the reader after the wrong cause;
- POST /user/account/totp/reset calls delete-totp before the setup mail goes out
  and then drops every session, so the old app stops working immediately and you
  are signed out. The HTML was right, the confirm dialog was wrong, and neither
  mentioned the sign-out.

The AppImage signing status is not evidenced anywhere in the tree, so the verify
box goes back to the scope main published: of the three installers only the .exe
carries a signature.

Words. /account said "TOTP" in the deactivate block and in the passkey field
while calling it the authenticator app everywhere else; it now says authenticator
app throughout, and the two kickers say what the block is for instead of which
standard implements it. /signup/verified made the reader rank seven authenticator
apps: it names two, keeps the two password managers, and links the help page that
compares them. Its step badge said 2 on a page with no step 1.

/download answered its own warning with four installers, three unsigned, and an
instruction to click past SmartScreen. The first screen now says what to do in
plain words, the evidence follows, and the installers sit behind "Still want the
outdated desktop version? (for administrators)".

Attribution. None of the eight screens named the party behind the product. The
legal-strip is stamped by apply-nav.py and stays untouched, so the company, the
town and the contact address are in the page body instead.

tests/ui-truthfulness.test.mjs re-pins the two sentences that changed and adds
eight assertions, one per finding, so none of them can come back.

Green locally: seo-contract, links, ui-truthfulness, frontend-loading-contract,
frontend-module-scripts, navigation-shell (24 checks), check-csp-inline.sh,
check-cache-bust.sh (330 links), eslint (exit 0). All eight pages re-measured at
390x844: scrollWidth === clientWidth on every one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJk2nCLCLmi3F71qkCUn7N
The five auth screens and /signup/verified each open with one sentence
saying what the screen is for and what happens after the button, per
docs/brand/messaging.md section 8: an auth screen does not sell, it
reports. Passkey is explained where it is clicked, not in a glossary.

Layout, measured at 390px with Playwright:

- request-reset and reset-confirm used <main class="page-main">, a class
  no stylesheet defines, so the heading and the primary button sat flush
  against both screen edges (0px). They now use container-sm, the same
  container as backup: 24px a side, and the button starts at 24px.
- .lede has no top margin and h1 no bottom margin, so on setup, backup
  and request-reset the heading touched the lead paragraph (0px, against
  8px on login and 16px on verified). Those three ledes take the
  existing .mt-3 utility, which leaves .lede alone for the eleven other
  pages that use it. Now 16px.

Text that did not match the code:

- reset-confirm claimed confirmation links last 15 minutes. The token is
  written with { EX: 3600 } (admin/server.js:1890, admin variant :3173),
  so it says 60 minutes now.
- reset-confirm promised the mail "arrives within a minute". Sending is
  fire-and-forget and nothing measures delivery, so the promise is gone.
  The same unsourced promise on verified ("up to 60 seconds") goes with
  it: the rule has to hold on every screen or it is not a rule.
- reset-confirm now says why the reset takes two mails. Clicking a link
  in a mail and then being told a second mail follows reads as phishing
  unless the page explains that the split is what stops someone who only
  had the mailbox open for a moment.
- the 429 on login blamed "this device". The limit is per IP (5/900s)
  and per email address (10/900s), admin/server.js:974-979, and the
  email counter is account-wide. It now names both counters.
- verified asked the reader to rank seven authenticator apps and called
  a SHA-256 app "stronger" without saying what that means. It now names
  two apps, says a password manager also works, and links the help page
  that does the comparing. The orphan step badge "2" on a page with no
  step 1 is a phrase instead of a number.

/js/passkey.js goes back to ?v=3: this branch no longer changes it.
Every fix in the previous commit gets an assertion that fails if the
sentence or the class is quietly dropped again:

- the opening sentence of each of the six screens, by name;
- no price, tier name or founder block on an auth screen, and no claim
  of a legal effect or a verified identity;
- <main> on request-reset and reset-confirm must carry a class that some
  stylesheet actually defines, which is what went wrong the first time;
- the 60 minute TTL, the absence of a delivery promise on both screens
  that used to make one, and the reason the reset takes two mails;
- the 429 text naming both counters instead of the device;
- the lead paragraph under the h1 on setup, backup and request-reset
  carrying a top margin;
- no app ranking and no orphan step badge on verified;
- the company and the country named on all six screens.

Scope: /account and /download are deliberately not asserted here. The
account screen is being rewritten in the dashboard PR and /download is
not an auth screen, so both are back at their main version on this
branch and their assertions came out with them.
POST /api/user/auth/request-totp-reset sends a confirmation mail, not a
setup link. The success message claimed the opposite.

- The confirmation token is { EX: 3600 } (admin/server.js:1890). The
  14 day token is only written after the confirmation is opened, in the
  reset-confirm handler (admin/server.js:1935), so at the moment this
  message appears no setup link exists yet. It now names both mails and
  puts each lifetime on the right one: 60 minutes for the confirmation,
  14 days for the setup link that follows it. /auth/reset-confirm was
  already saying this correctly, so the two screens now agree.
- The lede said "we send a link that lets you link a new authenticator
  app", which folds the two mails into one. Same correction.
- The success message could not be seen when it fired. #success sat
  inside #reset-form, and the handler hides that form on success, so the
  confirmation was hidden along with its parent and the screen went
  blank after submitting. It now sits after the form. This was already
  the case on main; it surfaced while rendering the new text. None of
  the other auth screens nest a result inside an element they hide.
- 429 had no branch of its own and fell through to "Try again", while
  the server answers retry_after 86400 off a limit of 5 per address per
  24 hours and 10 per connection per hour (admin/server.js:1875). It now
  says the wait can run to 24 hours, offers the support address, and
  leaves the button disabled, because retrying does not help.

Six sabotages, each reverted, all red: 60 to 99 minutes, the setup-link
wording back, the 14 days detached from the second mail, the 429 branch
removed, the 24 hours softened, and #success moved back inside the form.
@Apolloccrypt
Apolloccrypt merged commit 09d81d9 into main Sep 2, 2026
10 checks passed
@Apolloccrypt
Apolloccrypt deleted the feat/fe-auth branch September 5, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant