From 72ad0aa489a240fdac47b9991bda5e4cd8700b4e Mon Sep 17 00:00:00 2001 From: crazyracer98 <8631139+crazyracer98@users.noreply.github.com> Date: Fri, 30 Oct 2026 22:05:30 +0000 Subject: [PATCH 01/18] feat(auth): move the SPA to BFF sessions (I2) and drop oidc-client-ts (BF1) - src/auth/: GET /bff/user, login by navigation to /bff/login?returnTo=, POST /bff/logout with X-CSRF then navigate to logoutUrl, /bff/config runtime config - SDK 0.7.0 on /api/crud: credentials include, X-CSRF middleware, cacheScope from sub, onUnauthorized navigates to login; roles from /bff/user for UI only - drop oidc-client-ts and react-oidc-context; Vite dev proxy for /bff, /api and /.well-known to the dev BFF on :8787; stories, stubs and tests follow --- .storybook/stubs.tsx | 31 +++++---- package.json | 6 +- pnpm-lock.yaml | 57 +++-------------- src/App.test.tsx | 2 +- src/App.tsx | 8 +-- src/api/CrudApi.test.tsx | 8 +-- src/api/CrudApi.tsx | 33 +++------- src/api/client.test.ts | 32 +++++++--- src/api/client.ts | 35 +++++++---- src/api/health.ts | 7 ++- src/api/roles.test.ts | 33 +++------- src/api/roles.ts | 44 +++++-------- src/api/sdk.test.ts | 85 ++++++++++++++----------- src/api/sdk.ts | 45 +++++++++---- src/auth/AuthProvider.tsx | 39 ++++++++++++ src/auth/authContext.ts | 21 +++++++ src/auth/bff.test.ts | 106 +++++++++++++++++++++++++++++++ src/auth/bff.ts | 119 +++++++++++++++++++++++++++++++++++ src/auth/config.test.ts | 27 ++++++++ src/auth/config.ts | 62 +++++++++--------- src/main.tsx | 28 +++++---- src/pages/AdminPage.test.tsx | 11 ++-- src/pages/AdminPage.tsx | 17 ++--- src/pages/adminStub.ts | 4 +- src/vite-env.d.ts | 15 +---- vite.config.ts | 25 ++++---- 26 files changed, 592 insertions(+), 308 deletions(-) create mode 100644 src/auth/AuthProvider.tsx create mode 100644 src/auth/authContext.ts create mode 100644 src/auth/bff.test.ts create mode 100644 src/auth/bff.ts create mode 100644 src/auth/config.test.ts diff --git a/.storybook/stubs.tsx b/.storybook/stubs.tsx index 420b99a..b1753d1 100644 --- a/.storybook/stubs.tsx +++ b/.storybook/stubs.tsx @@ -1,14 +1,14 @@ -// Stubs for stories: a fake signed-in/out OIDC session and a fake backend, +// Stubs for stories: a fake signed-in/out BFF session and a fake backend, // both chosen per story through `parameters` (see preview.tsx). Stories never // touch Keycloak or the real API, so every render is deterministic. import type { ReactNode } from "react"; -import { AuthContext, type AuthContextProps } from "react-oidc-context"; +import { AuthContext, type AuthValue } from "../src/auth/authContext"; import type { Hero } from "../src/api/heroes"; export type AuthState = "signedOut" | "signedIn" | "loading"; export interface ApiStub { - /** `GET /api/health/live`: "pending" never answers (the "checking" state). */ + /** `GET /api/crud/health/live`: "pending" never answers (the "checking" state). */ health?: "up" | "down" | "pending"; /** `GET` of the heroes list: rows, "pending" (loading) or an HTTP error. */ heroes?: Hero[] | "pending" | { status: number; detail: string }; @@ -58,20 +58,22 @@ export const heroFixtures: Hero[] = [ const noop = async () => undefined; -export function stubAuth(state: AuthState): AuthContextProps { +export function stubAuth(state: AuthState): AuthValue { const signedIn = state === "signedIn"; return { isLoading: state === "loading", isAuthenticated: signedIn, user: signedIn - ? ({ - access_token: "stub-token", - profile: { preferred_username: "ada.lovelace" }, - } as AuthContextProps["user"]) + ? { + sub: "stub-sub", + name: "ada.lovelace", + roles: { api: ["maintainer"] }, + expiresAt: 4102444800, + } : null, - signinRedirect: noop, - removeUser: noop, - } as unknown as AuthContextProps; + login: () => undefined, + logout: noop, + }; } export function StubAuthProvider({ @@ -102,13 +104,16 @@ export function installApiStub(api: ApiStub = {}) { window.fetch = async (input, init) => { const url = new URL(String(input), window.location.origin); const method = (init?.method ?? "GET").toUpperCase(); - if (url.pathname === "/api/health/live") { + if (url.pathname === "/api/crud/health/live") { if (health === "pending") return never(); return health === "up" ? json(200, { status: "ok" }) : json(503, { title: "down" }); } - if (url.pathname === "/api/crud/v1/heroes/v2/json" && method === "GET") { + if ( + url.pathname === "/api/crud/crud/v1/heroes/v2/json" && + method === "GET" + ) { if (heroes === "pending") return never(); if (!Array.isArray(heroes)) { return json(heroes.status, { detail: heroes.detail }); diff --git a/package.json b/package.json index 39c032b..c2eb8ae 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "storybook:build": "storybook build" }, "dependencies": { - "@craftainer/crud-sdk": "0.5.1", + "@craftainer/crud-sdk": "0.7.0", "@craftainer/crud-views": "0.5.1", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", @@ -29,10 +29,8 @@ "@mui/x-data-grid": "9.16.0", "@tanstack/react-query": "5.104.5", "motion": "14.4.0", - "oidc-client-ts": "3.5.0", "react": "19.2.8", - "react-dom": "19.2.8", - "react-oidc-context": "3.3.1" + "react-dom": "19.2.8" }, "devDependencies": { "@axe-core/playwright": "4.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d674ddf..ef3e247 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,11 +133,11 @@ importers: .: dependencies: "@craftainer/crud-sdk": - specifier: 0.5.1 - version: 0.5.1(@tanstack/query-core@5.104.5) + specifier: 0.7.0 + version: 0.7.0(@tanstack/query-core@5.104.5) "@craftainer/crud-views": specifier: 0.5.1 - version: 0.5.1(319a24e500b9648a67dc6069b0d1fdef) + version: 0.5.1(e73a0e164bfebe3c31f29ec740c43035) "@emotion/react": specifier: 11.14.0 version: 11.14.0(@types/react@19.2.18)(react@19.2.8) @@ -168,18 +168,12 @@ importers: motion: specifier: 14.4.0 version: 14.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - oidc-client-ts: - specifier: 3.5.0 - version: 3.5.0 react: specifier: 19.2.8 version: 19.2.8 react-dom: specifier: 19.2.8 version: 19.2.8(react@19.2.8) - react-oidc-context: - specifier: 3.3.1 - version: 3.3.1(oidc-client-ts@3.5.0)(react@19.2.8) devDependencies: "@axe-core/playwright": specifier: 4.14.0 @@ -408,10 +402,10 @@ packages: } hasBin: true - "@craftainer/crud-sdk@0.5.1": + "@craftainer/crud-sdk@0.7.0": resolution: { - integrity: sha512-YFnjN4tFr4MJDo0MiwvUasoIB/T3QtiW3nYX9wGxmYqtU/3NsYfGKlFLOi4U37REkHEH51tNy/L5Ccza+eJjhw==, + integrity: sha512-K2BQ0N+goByY+dirDDno59N3o4MDCmaF+yUwq/qtD+fMkH2CYcWZaz2+DJjUgBEvDIHAJHYh6EJ5xOHvvV304w==, } engines: { node: ">=24.22.0" } hasBin: true @@ -2904,13 +2898,6 @@ packages: integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==, } - jwt-decode@4.0.0: - resolution: - { - integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==, - } - engines: { node: ">=18" } - lightningcss-android-arm64@1.33.0: resolution: { @@ -3160,13 +3147,6 @@ packages: } engines: { node: ">=12.20.0" } - oidc-client-ts@3.5.0: - resolution: - { - integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==, - } - engines: { node: ">=18" } - open@10.2.0: resolution: { @@ -3361,16 +3341,6 @@ packages: integrity: sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==, } - react-oidc-context@3.3.1: - resolution: - { - integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==, - } - engines: { node: ">=18" } - peerDependencies: - oidc-client-ts: ^3.1.0 - react: ">=16.14.0" - react-transition-group@4.4.5: resolution: { @@ -4016,13 +3986,13 @@ snapshots: dependencies: css-tree: 3.2.1 - "@craftainer/crud-sdk@0.5.1(@tanstack/query-core@5.104.5)": + "@craftainer/crud-sdk@0.7.0(@tanstack/query-core@5.104.5)": optionalDependencies: "@tanstack/query-core": 5.104.5 - "@craftainer/crud-views@0.5.1(319a24e500b9648a67dc6069b0d1fdef)": + "@craftainer/crud-views@0.5.1(e73a0e164bfebe3c31f29ec740c43035)": dependencies: - "@craftainer/crud-sdk": 0.5.1(@tanstack/query-core@5.104.5) + "@craftainer/crud-sdk": 0.7.0(@tanstack/query-core@5.104.5) "@emotion/react": 11.14.0(@types/react@19.2.18)(react@19.2.8) "@emotion/styled": 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) "@mui/material": 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -5340,8 +5310,6 @@ snapshots: jsonc-parser@3.3.1: {} - jwt-decode@4.0.0: {} - lightningcss-android-arm64@1.33.0: optional: true @@ -5447,10 +5415,6 @@ snapshots: obug@2.1.4: {} - oidc-client-ts@3.5.0: - dependencies: - jwt-decode: 4.0.0 - open@10.2.0: dependencies: default-browser: 5.5.1 @@ -5617,11 +5581,6 @@ snapshots: react-is@19.3.1: {} - react-oidc-context@3.3.1(oidc-client-ts@3.5.0)(react@19.2.8): - dependencies: - oidc-client-ts: 3.5.0 - react: 19.2.8 - react-transition-group@4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: "@babel/runtime": 7.29.7 diff --git a/src/App.test.tsx b/src/App.test.tsx index 3b39ab0..8f9d8b6 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import App from "./App"; const { useAuthMock } = vi.hoisted(() => ({ useAuthMock: vi.fn() })); -vi.mock("react-oidc-context", () => ({ useAuth: useAuthMock })); +vi.mock("./auth/authContext", () => ({ useAuth: useAuthMock })); describe("App", () => { // Vitest globals are off, so Testing Library does not clean up on its own. diff --git a/src/App.tsx b/src/App.tsx index 146f271..8c14020 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import Container from "@mui/material/Container"; import Stack from "@mui/material/Stack"; import Toolbar from "@mui/material/Toolbar"; import Typography from "@mui/material/Typography"; -import { useAuth } from "react-oidc-context"; +import { useAuth } from "./auth/authContext"; import { HealthStatus } from "./components/HealthStatus"; import { AdminPage } from "./pages/AdminPage"; @@ -33,9 +33,9 @@ function App() { noWrap sx={{ maxWidth: { xs: "6rem", sm: "none" } }} > - {auth.user?.profile.preferred_username} + {auth.user?.name} - @@ -43,7 +43,7 @@ function App() {