docs(plans): session-expiry soft redirect — 3-task plan (#48)
This commit is contained in:
@@ -135,12 +135,22 @@ if (!user) {
|
||||
No `reason` here — an unauthenticated deep-link is not an expiry; login simply returns the user to `from`.
|
||||
|
||||
### `shell/user-menu.tsx` (logout pending state)
|
||||
Disable the Sign out item while the mutation is in flight:
|
||||
`<MenuItem disabled={logout.isPending} onClick={onSignOut}>`. Error already surfaces via the global toast.
|
||||
Keep the menu open during logout and show a pending state on the item (Base UI `MenuItem` supports both
|
||||
`closeOnClick` and `disabled`):
|
||||
```tsx
|
||||
<MenuItem closeOnClick={false} disabled={logout.isPending} onClick={onSignOut}>
|
||||
{logout.isPending ? t("auth.signingOut") : t("auth.signOut")}
|
||||
</MenuItem>
|
||||
```
|
||||
On success the mutation sets `me` to null and navigates to `/login`, so `UserMenu` unmounts (it returns
|
||||
`null` when `!me`). Logout **error** already surfaces via the global toast (`useLogout` has no
|
||||
`suppressErrorToast`). `closeOnClick={false}` also prevents a double-submit (the item is disabled while
|
||||
pending).
|
||||
|
||||
### i18n (en + sv parity — 1 new key)
|
||||
### i18n (en + sv parity — 2 new keys)
|
||||
- `auth.sessionExpired` = "Your session expired — please sign in again." /
|
||||
"Din session har gått ut — logga in igen."
|
||||
- `auth.signingOut` = "Signing out…" / "Loggar ut…"
|
||||
|
||||
## Data flow
|
||||
API 401 → `client.ts` middleware → `redirectToLogin()` → registered `navigate("/login?reason=expired&
|
||||
@@ -170,9 +180,10 @@ same return-to. The `NavigationBridge` keeps `setNavigate` current for the lifet
|
||||
(`//evil.com`) falls back to `/objects`; the submit button is disabled until both fields are non-empty.
|
||||
- **`auth/require-auth` test:** when `useMe` resolves to no user, it renders a redirect to
|
||||
`/login?from=…` carrying the attempted path.
|
||||
- **`shell/user-menu` test:** the Sign out item is disabled while `logout.isPending`.
|
||||
- **`shell/user-menu` test:** with a delayed logout handler, after clicking Sign out the item shows the
|
||||
`auth.signingOut` pending text (the menu stays open via `closeOnClick={false}`).
|
||||
- **Gate:** `typecheck`/`lint`/`test`/`build`/`check:size`/`check:colors` green; en/sv parity (the #60
|
||||
parity test guards the new key); no codename; no new dependency.
|
||||
parity test guards the 2 new keys); no codename; no new dependency.
|
||||
|
||||
## Acceptance criteria
|
||||
1. A 401 from an API call performs a **router** navigation (no full page reload) to
|
||||
@@ -182,10 +193,10 @@ same return-to. The `NavigationBridge` keeps `setNavigate` current for the lifet
|
||||
values are rejected.
|
||||
3. `RequireAuth` redirects unauthenticated users to `/login?from=<attempted path>` so deep links return
|
||||
after login.
|
||||
4. Login submit is disabled while either field is empty; the Sign out item shows a disabled/pending state
|
||||
while logging out.
|
||||
5. `typecheck`/`lint`/`test`/`build`/`check:colors` green; `check:size` reported; en/sv parity (1 new
|
||||
key); no codename; no new dependency.
|
||||
4. Login submit is disabled while either field is empty; the Sign out item shows a pending state
|
||||
(`auth.signingOut`, disabled) while logging out.
|
||||
5. `typecheck`/`lint`/`test`/`build`/`check:colors` green; `check:size` reported; en/sv parity (2 new
|
||||
keys); no codename; no new dependency.
|
||||
|
||||
## Out of scope → follow-ups
|
||||
- **In-place re-auth modal** that keeps the edit form mounted for full field-level preservation of
|
||||
|
||||
Reference in New Issue
Block a user