From f79b89b02677ed68e3aa271905951692e81a3501 Mon Sep 17 00:00:00 2001 From: User Date: Sat, 22 Aug 2026 17:55:58 +0200 Subject: [PATCH] docs: plan local-only logout visibility --- ...2026-08-22-local-only-logout-visibility.md | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-22-local-only-logout-visibility.md diff --git a/docs/superpowers/plans/2026-08-22-local-only-logout-visibility.md b/docs/superpowers/plans/2026-08-22-local-only-logout-visibility.md new file mode 100644 index 00000000..bb85108e --- /dev/null +++ b/docs/superpowers/plans/2026-08-22-local-only-logout-visibility.md @@ -0,0 +1,337 @@ +# Local-Only Logout Visibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show ThothII's `Log out` control only for standalone installations using local authentication, while keeping it absent from the Aritmolab server and every other non-local mode. + +**Architecture:** Reuse the already validated `GET /auth/config` response as the single runtime authority. `AuthGate` derives `canLogout` from `config.mode === "local"`, passes that explicit capability into `AppShell`, and `AppShell` conditionally renders only the existing button while leaving identity display and logout mechanics unchanged. + +**Tech Stack:** React 18, TypeScript 5.6, Vitest 2, React Testing Library, MSW, Vite 6. + +## Global Constraints + +- Source baseline is `6475ea3`; the approved design is `docs/superpowers/specs/2026-08-22-local-only-logout-visibility-design.md`. +- `GET /auth/config` is the sole authority. Do not add platform, hostname, URL, issuer, packaging, or build-time detection. +- The exact visibility rule is `config.mode === "local"`; `upstream`, `oidc`, `none`, `mock`, an absent config, or an invalid config must never enable the control. +- Keep the authenticated username or display name visible in every mode. +- Preserve the existing `POST /auth/logout` route, logout coordinator, authentication-state cleanup, query cleanup, and session/transcript isolation behavior. +- Do not change backend, Compose, native `tht`, deployment, or packaging files. +- Do not add dependencies or change the English UI string `Log out`. +- Use strict TDD: add the focused regression tests, observe the intended RED, implement the minimum change, then run focused and full GREEN gates. +- Preserve and do not stage the pre-existing changes in `.superpowers/sdd/progress.md`, `brain/index.md`, and `brain/codebase/psd-dwh-transport.md`. + +--- + +### Task 1: Propagate the local-auth capability and conditionally render logout + +**Files:** +- Modify: `frontend/src/auth/AuthGate.test.tsx:10-17,63-68` +- Modify: `frontend/src/shell/AppShell.auth.test.tsx:11-28,45-89,90-153` +- Modify: `frontend/src/auth/AuthGate.tsx:20-29,79-81` +- Modify: `frontend/src/shell/AppShell.tsx:39-40,715-722` +- Modify: `frontend/src/shell/AppShell.new-session.test.tsx:12-15` +- Modify: `frontend/src/shell/AppShell.session-mgmt.test.tsx:22-26` +- Modify: `frontend/src/shell/AppShell.session-target.test.tsx:12-15` + +**Interfaces:** +- Consumes: validated `AuthPublicConfig.mode` from the existing `getAuthConfig()` call in `AuthGate`. +- Produces: + +```ts +interface AppShellProps { + canLogout: boolean; +} + +export function AppShell({ canLogout }: AppShellProps): JSX.Element; +``` + +- `canLogout` is required at every `AppShell` call site. Production passes + `config?.mode === "local"`; focused non-auth shell tests pass `false`; existing local-auth shell + tests pass `true`. + +- [ ] **Step 1: Add RED tests for capability propagation and shell visibility** + +In `frontend/src/auth/AuthGate.test.tsx`, replace the `AppShell` mock with a capability-observing +mock: + +```tsx +vi.mock("../shell/AppShell", () => ({ + AppShell: ({ canLogout }: { canLogout: boolean }) => ( +
+ Authenticated shell + {canLogout && ( + + )} +
+ ), +})); +``` + +Extend the existing local authenticated-shell test so it proves the positive contract: + +```tsx +test("renders the authenticated shell from the safe /me DTO", async () => { + render(); + + const shell = await screen.findByTestId("authenticated-shell"); + expect(shell).toHaveAttribute("data-can-logout", "true"); + expect(screen.getByRole("button", { name: "Log out" })).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: /sign in/i })).not.toBeInTheDocument(); +}); +``` + +Add an upstream regression beside it: + +```tsx +test("disables shell logout for trusted upstream authentication", async () => { + server.use( + http.get("/api/auth/config", () => HttpResponse.json({ + mode: "upstream", localLogin: false, oidcLogin: false, + })), + http.get("/api/me", () => HttpResponse.json({ + issuer: "portal", + subject: "portal-user", + displayName: "Portal user", + roles: ["user"], + permissions: ["session.use"], + isAdmin: false, + csrfToken: null, + session: null, + })), + ); + + render(); + + const shell = await screen.findByTestId("authenticated-shell"); + expect(shell).toHaveAttribute("data-can-logout", "false"); + expect(screen.queryByRole("button", { name: "Log out" })).not.toBeInTheDocument(); +}); +``` + +In `frontend/src/shell/AppShell.auth.test.tsx`, add a `canLogout` argument to the existing helper +and make the keyed/direct local-auth renders explicit: + +```tsx +function renderShell(user: { + subject: string; + isAdmin: boolean; + roles: readonly ("user" | "admin")[]; + permissions: readonly string[]; +}, canLogout = true) { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + setAuthState({ + issuer: "local", ...user, csrfToken: null, session: null, + }); + return render( + + + , + ); +} + +function KeyedAuthenticatedShell() { + const user = useAuthUser(); + const generation = useAuthGeneration(); + return user + ? + : null; +} +``` + +Change the direct render in the stale-logout test to ``. Then add this +negative shell test before the existing local logout test: + +```tsx +test("hides logout outside local authentication while preserving the identity", async () => { + let logoutCalls = 0; + server.use(http.post("/api/auth/logout", () => { + logoutCalls += 1; + return new HttpResponse(null, { status: 204 }); + })); + + renderShell({ + subject: "portal-user", + isAdmin: false, + roles: ["user"], + permissions: ["session.use"], + }, false); + + expect(await screen.findByText("portal-user")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Log out" })).not.toBeInTheDocument(); + expect(logoutCalls).toBe(0); +}); +``` + +- [ ] **Step 2: Run the focused tests and capture RED** + +Run from `frontend/`: + +```bash +npx vitest run src/auth/AuthGate.test.tsx src/shell/AppShell.auth.test.tsx +``` + +Expected: FAIL. The local `AuthGate` test receives no `canLogout` prop, and the real `AppShell` +still renders `Log out` when the new negative test passes `false`. + +- [ ] **Step 3: Implement the minimal explicit capability flow** + +In `frontend/src/auth/AuthGate.tsx`, replace `AuthenticatedContent` with: + +```tsx +function AuthenticatedContent({ + canLogout, + onExpired, +}: { + canLogout: boolean; + onExpired: () => void; +}) { + const user = useAuthUser(); + const authGeneration = useAuthGeneration(); + useEffect(() => { + if (!user) onExpired(); + }, [onExpired, user]); + return user + ? + : null; +} +``` + +Replace the authenticated branch with the closed local-only rule: + +```tsx +if (status === "authenticated") { + return ( + setStatus("login")} + /> + ); +} +``` + +In `frontend/src/shell/AppShell.tsx`, add the required prop at the component boundary: + +```tsx +interface AppShellProps { + canLogout: boolean; +} + +export function AppShell({ canLogout }: AppShellProps) { + const authenticatedUser = useAuthUser(); +``` + +Keep the identity row unchanged and wrap only the existing button: + +```tsx +{authenticatedUser && ( +
+ + {authenticatedUser.displayName ?? authenticatedUser.subject} + + {canLogout && ( + + )} +
+)} +``` + +Update the three non-authentication shell test helpers so their intent is explicit and they do +not render an irrelevant logout control: + +```tsx +// frontend/src/shell/AppShell.new-session.test.tsx +return render( + + + , +); + +// frontend/src/shell/AppShell.session-mgmt.test.tsx +return render( + + + , +); + +// frontend/src/shell/AppShell.session-target.test.tsx +return render( + + + , +); +``` + +Do not alter `signOut()`, `logoutUser()`, `frontend/src/api/auth.ts`, or any backend route. + +- [ ] **Step 4: Run the focused tests and confirm GREEN** + +Run from `frontend/`: + +```bash +npx vitest run src/auth/AuthGate.test.tsx src/shell/AppShell.auth.test.tsx +``` + +Expected: both test files PASS, including the new local-positive and upstream-negative cases. + +- [ ] **Step 5: Prove every `AppShell` caller supplies the capability** + +Run from `frontend/`: + +```bash +npx tsc -b +``` + +Expected: exit code 0 with no TypeScript diagnostics. A missing `canLogout` at any production or +test call site is a compile error. + +- [ ] **Step 6: Run complete frontend verification** + +Run from `frontend/`: + +```bash +npx vitest run +npm run build +``` + +Expected: the complete Vitest suite passes; the build completes TypeScript project checking and +Vite production bundling with exit code 0. + +- [ ] **Step 7: Review scope and commit the implementation** + +Run from the repository root: + +```bash +git diff --check +git diff -- frontend/src/auth/AuthGate.tsx \ + frontend/src/auth/AuthGate.test.tsx \ + frontend/src/shell/AppShell.tsx \ + frontend/src/shell/AppShell.auth.test.tsx \ + frontend/src/shell/AppShell.new-session.test.tsx \ + frontend/src/shell/AppShell.session-mgmt.test.tsx \ + frontend/src/shell/AppShell.session-target.test.tsx +git status --short +``` + +Expected: no whitespace errors; the implementation diff contains only capability propagation, +conditional button rendering, and the corresponding test updates. The pre-existing brain and SDD +files remain unstaged. + +Commit only the implementation files: + +```bash +git add frontend/src/auth/AuthGate.tsx \ + frontend/src/auth/AuthGate.test.tsx \ + frontend/src/shell/AppShell.tsx \ + frontend/src/shell/AppShell.auth.test.tsx \ + frontend/src/shell/AppShell.new-session.test.tsx \ + frontend/src/shell/AppShell.session-mgmt.test.tsx \ + frontend/src/shell/AppShell.session-target.test.tsx +git commit -m "fix(frontend): hide logout outside local auth" +``` + +Expected: one commit containing only the seven listed frontend files. Do not stage or commit the +pre-existing workspace changes.