Skip to content

refactor(errors): type the ?error= codes on /login, /account and /admin/accounts - #89

Merged
guarzo merged 1 commit into
mainfrom
worktree-typed-error-codes
Aug 4, 2026
Merged

refactor(errors): type the ?error= codes on /login, /account and /admin/accounts#89
guarzo merged 1 commit into
mainfrom
worktree-typed-error-codes

Conversation

@guarzo

@guarzo guarzo commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #88. Follow-up to #79; completes what #85 deliberately left.

What changed

Three pages render an operator-facing notice keyed off ?error=, and all three kept Record<string, string> maps — the thing that defeats the type system, since it accepts any key and so keyof typeof yields string and derives nothing. A code with no entry renders nothing at all: the redirect happens, the page loads unchanged, and the member gets no explanation.

Unlike payouts there was no chokepoint to type. Six producers wrote the URL by hand, and two of them pick between two different destination pages in a single ternary, so helpers had to be introduced before anything could be typed.

New src/lib/error-redirects.ts holds three as const maps, a union per map, three builders, and lookupErrorMessage.

The two architectural calls

Builders returning a string, not #85's : never redirectors. Two incompatible mechanisms consume these. Server actions and page guards call redirect() from next/navigation, which returns never — and denyAdmin and redirectOnMutationError both depend on that never for their exhaustiveness over a service error union. The OAuth callbacks are route handlers using a local to() wrapping NextResponse.redirect, which must return a Response. A : never helper cannot be returned from a route handler; a Response-returning one would destroy the never the two switches rely on. A string sits underneath both and neither loses anything:

return to(sess ? accountErrorUrl("link_expired") : loginErrorUrl("session_expired"));

Each branch is checked against its own page's union. A per-file helper could not do that — the code and its destination have to be typed together.

Codes live in src/lib/, not src/app/. src/lib/admin-guard.ts redirects to /account?error=not_admin, and grep -rn 'from "@/app' src/lib src/core src/services src/jobs src/worker returns nothing — nothing outside src/app/ imports src/app/ anywhere in this repo. Colocating would have made admin-guard the first inversion, for a UI string. admin-guard already hardcoded that exact URL and already imports next/navigation; this only names what was there, and the new module imports nothing itself.

src/app/payouts/errors.ts stays put — it has no src/lib producer, so colocation is still correct there. Same rule, two answers, because the producer sets differ. lookupErrorMessage moves to the shared module and payouts re-exports it, so both payout pages' imports are unchanged.

Two details worth a look

  • redirectOnMutationError is now exhaustive on a second, independent axis, and its docblock says so: the service union says which failures exist, the code union says which ones the page can explain. The ?error= axis extends the existing switch rather than sitting beside it.
  • adminAccountsErrorUrl preserves ?tier=pending by construction via URLSearchParams — the way createFailed does it — instead of by remembering to include it.

Non-goal honoured: codes are not globally unique

not_admin stays in both /account and /admin/accounts with different copy. /account is where a genuinely de-roled admin is sent ("your admin access was removed"); /admin/accounts is reached by a stale tab whose actor lost the bit between render and click ("refresh to see the current state"). One message would be wrong on one of them. session_expired likewise reaches /login from four unrelated producers. Three maps in one file: one shape, three namespaces.

The guard, demonstrated

Six deliberate breaks, all caught by npm run typecheck, all reverted before commit:

Break Error
loginErrorUrl("session_expred") TS2345: Argument of type '"session_expred"' is not assignable…
accountErrorUrl("stale_charater") TS2345: Argument of type '"stale_charater"' is not assignable…
accountErrorUrl("not_pending") — valid on /admin/accounts, wrong page TS2345: … not assignable to parameter of type '"already_linked" | … | "not_admin"'
loginErrorUrl("not_admin") — valid on both other pages TS2345: … not assignable to parameter of type '"oauth_denied" | … | "session_expired"'
adminAccountsErrorUrl("session_expired") inside the exhaustive switch TS2345: … not assignable to parameter of type '"not_admin" | "last_admin" | "not_pending" | "not_found"'
Deleting the stale_character map entry TS2345 at both producers — the original failure mode, now a compile error

Rows 3 and 4 are the cases a single unified map would have missed; row 4 is a code valid on two other surfaces. Row 6 is the failure this whole change exists to prevent.

No live bug surfaced, no copy changed

Every code the six producers emit already had an entry in its destination map — exact set equality on all three, no gaps and no orphans. Nothing needed inventing.

Message text moved byte-for-byte: extracted map bodies diffed against HEAD, all three IDENTICAL. Every generated URL matches the literal it replaced, ?tier=pending&error= param order included.

Also: two stale comment references from #85

Both said "the ERRORS map", a symbol that no longer exists in payouts. Names only — surrounding rationale untouched.

  • src/app/payouts/dropped.tsOPERATION_ERRORS
  • e2e/payouts.spec.tsNEW_OPERATION_ERRORS / OPERATION_ERRORS

Scope

The e2e specs stay exactly as they are; #79's suggestion to thin them now that types hold the invariant is still a separate change. /admin/accounts's notice guard changed only because params.error && ERRORS[params.error] stops compiling against an as const map — it is now one errorMessage &&, same behaviour.

One thing the task description missed and this covers: src/app/account/page.tsx is a sixth producer of /login?error=session_expired, alongside the five listed. Left hand-written it would have been the most-emitted code on that page sitting outside the guard.

No migration. No change to OAuth state consumption, cookie handling, or consumeOauthTransaction — only which URL a failure redirects to.

Verification

  • npm run typecheck → clean (tsc --noEmit, no output)
  • npm testTest Files 71 passed (71), Tests 869 passed (869)
  • npm run test:e2e -- account.spec.ts18 passed (15.1s)
  • npm run test:e2e -- admin.spec.ts36 passed (42.4s)
  • npm run format:checkAll matched files use Prettier code style!
  • npm run lint0 errors, 3 warnings (pre-existing <img> warnings, untouched files)

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling across login, account, administration, authentication, and payout flows.
    • Redirects now consistently lead to the appropriate error pages for expired sessions, authorization issues, invalid requests, and failed operations.
    • Unknown or invalid error codes no longer display misleading messages.
    • Preserved relevant payout filters when administrative errors occur.
  • Documentation

    • Clarified error-code behavior and validation coverage in developer-facing documentation.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e50681da-7de5-41ef-830c-cd2b8e5f360e

📥 Commits

Reviewing files that changed from the base of the PR and between a15572f and 961feba.

📒 Files selected for processing (12)
  • e2e/payouts.spec.ts
  • src/app/account/actions.ts
  • src/app/account/page.tsx
  • src/app/admin/accounts/actions.ts
  • src/app/admin/accounts/page.tsx
  • src/app/auth/discord/callback/route.ts
  • src/app/auth/eve/callback/route.ts
  • src/app/login/page.tsx
  • src/app/payouts/dropped.ts
  • src/app/payouts/errors.ts
  • src/lib/admin-guard.ts
  • src/lib/error-redirects.ts
📝 Walkthrough

Walkthrough

The PR adds shared typed error maps, redirect builders, and safe message lookup. It applies them to payout, account, login, OAuth, and admin-account flows, replacing local maps and hardcoded error URLs.

Changes

Application error handling

Layer / File(s) Summary
Shared redirect and lookup contracts
src/lib/error-redirects.ts
Defines typed error maps, URL builders, optional pending-tier preservation, and safe lookup for unknown codes.
Payout error contracts and consumers
src/app/payouts/errors.ts, src/app/payouts/actions.ts, src/app/payouts/new/page.tsx, src/app/payouts/[id]/page.tsx, src/app/payouts/dropped.ts, e2e/payouts.spec.ts
Centralizes payout error messages and types. Payout actions use typed error codes, and payout pages use shared message lookup.
Account and authentication error routing
src/app/account/*, src/app/login/page.tsx, src/app/auth/discord/callback/route.ts, src/app/auth/eve/callback/route.ts
Replaces hardcoded account and login error URLs and local message maps with shared helpers and definitions.
Admin-account error routing
src/app/admin/accounts/*, src/lib/admin-guard.ts
Routes admin errors through the shared builder and resolves admin messages through the shared map while preserving pending-tier parameters.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • #88 — Types the ?error= codes for login, account, and admin-account surfaces, which this PR implements.
  • #79 — Covers the related payout error-map centralization and typed error-code handling.

Possibly related PRs

  • guarzo/authGD#83 — Shares payout error-code handling changes across the same payout files.
  • guarzo/authGD#4 — Shares admin-account action, page, and guard changes.
  • guarzo/authGD#1 — Shares account, authentication callback, and account-page changes.

Poem

A rabbit maps each error trail,
With typed redirects set to sail.
Payout notices now appear,
Login paths grow crisp and clear.
“Hop!” says the bunny, “bugs beware!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: typing error codes for the login, account, and admin accounts pages.
Description check ✅ Passed The description is detailed and covers the changes, rationale, verification, scope, and deployment impact, despite missing some template headings.
Linked Issues check ✅ Passed The changes satisfy issue #88 by adding page-specific typed error maps, redirect helpers, query preservation, and exhaustive validation without changing copy or migrations.
Out of Scope Changes check ✅ Passed All changes align with issue #88, including shared payout lookup updates and stale comment corrections explicitly included in the stated scope.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-typed-error-codes
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-typed-error-codes
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch worktree-typed-error-codes

Comment @coderabbitai help to get the list of available commands.

…in/accounts

Follow-up to #79. #85 did this for the two payout pages and named what it
skipped: these three still carried untyped `Record<string, string>` maps, which
is the thing that defeats the type system — it accepts any key, so
`keyof typeof` yields `string` and derives nothing. A code with no entry in the
destination page's map renders nothing at all.

Unlike payouts there was no chokepoint to type: six producers wrote the URL by
hand, and two of them pick between two DIFFERENT destination pages in a single
ternary. So the helpers had to be introduced before anything could be typed.

Shape: typed URL BUILDERS returning a string, not payouts' `: never`
redirectors. Two incompatible mechanisms consume these — server actions and
page guards call `redirect()` (returns `never`, which `denyAdmin` and
`redirectOnMutationError` both depend on for their exhaustiveness over a
service error union), while the OAuth callbacks are route handlers that must
return a Response. A `: never` helper cannot be returned from a route handler,
and a Response-returning one would destroy the `never` the two switches rely
on. A string sits underneath both. It also lets one call site pick between two
destinations with each branch checked against its own page's union.

Home: `src/lib/error-redirects.ts`, not `src/app/`. `src/lib/admin-guard.ts`
produces one of these codes, and nothing outside `src/app/` imports `src/app/`
anywhere in this repo; colocating would have made admin-guard the first
inversion, for a UI string. admin-guard already hardcoded that URL and already
imports next/navigation. `src/app/payouts/errors.ts` stays where it is — it has
no `src/lib` producer, so colocation is still right there. `lookupErrorMessage`
moves to the shared module and payouts re-exports it.

`redirectOnMutationError` is now exhaustive on a second, independent axis and
its docblock says so: the service union says which failures exist, the code
union says which ones the page can explain. `adminAccountsErrorUrl` preserves
`?tier=pending` via URLSearchParams, the way `createFailed` does.

Non-goal honoured: codes are NOT globally unique. `not_admin` stays in both
/account and /admin/accounts with different copy — one is the de-roled admin's
destination, the other a stale-tab race — and `session_expired` reaches /login
from four unrelated producers.

No live bug surfaced: every emitted code already had an entry in its
destination map. Copy moved byte-for-byte (extracted map bodies diffed against
HEAD: all three IDENTICAL) and every generated URL matches the literal it
replaced, `?tier=pending&error=` param order included.

Also fixes two stale comment references from #85 naming an `ERRORS` symbol that
no longer exists in payouts (dropped.ts, e2e/payouts.spec.ts) — names only.

No migration. No change to OAuth state consumption, cookie handling or
consumeOauthTransaction; only which URL a failure redirects to.
@guarzo
guarzo force-pushed the worktree-typed-error-codes branch from 8860388 to 961feba Compare August 4, 2026 22:26
@guarzo
guarzo enabled auto-merge (squash) August 4, 2026 22:27
@guarzo
guarzo merged commit 31c390b into main Aug 4, 2026
6 checks passed
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.

Type the ?error= codes on /login, /account and /admin/accounts — the three surfaces #85 left

2 participants