diff --git a/AGENTS.md b/AGENTS.md index 858175ccb..393aed77d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,6 +208,11 @@ A run-bearing analysis-run registry empties only after an unrevoked (ADR 0020 / v0.87.0). The documented phrase is not a secret. Do not expose purge on a public HTTP route. +Unauthenticated **Log in** must call `returnUrlFromLocation()` then +`rememberOidcReturnUrl` before `signinRedirect` (ADR 0109) so a +shared `/?post=` link still opens that post. Do not mount tenant admin +settings on the signed-out login shell. + `POST /api/analysis-runs` records Pending lineage only (ADR 0017 / v2.7.1). TEPP and period-report kinds 422 before any snapshot write. `POST /api/analysis-runs/{id}/start` reconstructs a Pending lineage diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d4ff2e159..4cb521e8a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -301,7 +301,9 @@ config baked in at build time from the same `.env` ports every other service uses (Vite embeds `import.meta.env.VITE_*` at build time, not runtime, so these are Docker build args, not container env vars). `src/App.test.tsx` mocks `react-oidc-context`'s `useAuth` to test the -component's own render logic (login button -> `signinRedirect()`; the +component's own render logic (login button stores a safe return path +then `signinRedirect()`; the signed-out shell never mounts tenant +admin settings; the A-100 fork DAG shows a branch point and rec-006 as its own root; `post_admin` can rebuild; fetch posts with the token -> render list -> click -> popup shows the fetched body and every panel; ask a chat diff --git a/CHANGELOG.d/2.12.19-oidc-login-return-remember.md b/CHANGELOG.d/2.12.19-oidc-login-return-remember.md new file mode 100644 index 000000000..1d590260f --- /dev/null +++ b/CHANGELOG.d/2.12.19-oidc-login-return-remember.md @@ -0,0 +1,6 @@ +## 2.12.19 — Remember the login return path + +- Log in now stores a validated same-origin return path (ADR 0109) + before the OIDC redirect, so a shared `/?post=` link still opens that + post after callback. Tenant admin settings stay off the signed-out + login shell so the production frontend build type-checks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 52390dabc..6959d29b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,6 +203,16 @@ All notable changes to this project are documented here. Format follows - The static SQL review contract now counts the Customer Master evidence query that uses closed schema fragments and bound entity ids. +## [2.12.19] - 2026-08-24 + +### Fixed + +- Log in remembers a validated same-origin return path (ADR 0109) before + the OIDC redirect, so a shared `/?post=` link still opens that post + after callback. Tenant admin settings stay off the signed-out login + shell. The production frontend build type-checks again. + + ## [2.12.6] - 2026-08-20 ### Added diff --git a/docs/adr/0109-oidc-deep-link-state-recovery.md b/docs/adr/0109-oidc-deep-link-state-recovery.md index 808f12aff..832a4b70a 100644 --- a/docs/adr/0109-oidc-deep-link-state-recovery.md +++ b/docs/adr/0109-oidc-deep-link-state-recovery.md @@ -21,6 +21,11 @@ when the member's OIDC session is otherwise valid. - Persist the same validated same-origin path in both `sessionStorage` and `localStorage` before redirecting to OIDC. `localStorage` is only a bounded recovery fallback, not an authentication or authorization store. +- The signed-out **Log in** control computes that path with + `returnUrlFromLocation()`, persists it with `rememberOidcReturnUrl`, and + passes the same value in `signinRedirect` state. +- Tenant admin settings mount only after authentication provides an access + token; the signed-out login shell never renders `AdminPanel`. - On callback, remove the key from both stores and use session storage before local storage. Reject external and protocol-relative URLs. - Keep member language preference account-scoped in diff --git a/frontend/package.json b/frontend/package.json index 56b813d28..5261a4354 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.6", + "version": "2.12.19", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 4a9ad9ec9..bfde3b7b0 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -32,6 +32,8 @@ beforeEach(() => { afterEach(() => { vi.unstubAllGlobals(); + window.sessionStorage.clear(); + window.localStorage.clear(); }); describe("App, unauthenticated", () => { @@ -55,6 +57,27 @@ describe("App, unauthenticated", () => { ); }); + it("remembers a same-origin post deep link before the OIDC redirect", async () => { + window.history.replaceState({}, "", "/?post=synthetic-post-ada"); + render(); + await userEvent.click(screen.getByRole("button", { name: /log in/i })); + expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toBe( + "/?post=synthetic-post-ada", + ); + expect(window.localStorage.getItem("lineageweave.oidc.returnUrl")).toBe( + "/?post=synthetic-post-ada", + ); + expect(signinRedirect).toHaveBeenCalledWith({ + state: { returnUrl: "/?post=synthetic-post-ada" }, + }); + }); + + it("does not mount tenant admin settings before authentication", () => { + render(); + expect(screen.queryByRole("heading", { name: /admin settings/i })).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/tenant brand name/i)).not.toBeInTheDocument(); + }); + it("does not render raw OIDC error text and names a log-in next action", async () => { mockAuth = { ...mockAuth, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83fc9acee..b1a5fbddb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -148,6 +148,7 @@ import { tf, useLocale, } from "./i18n"; +import { rememberOidcReturnUrl, returnUrlFromLocation } from "./oidcReturnUrl"; import "./App.css"; function orchestratorUnavailableMessage(err: unknown, action: string): string { @@ -6292,7 +6293,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean description={t("Log in again to open the workspace.")} retryLabel={t("Log in")} onRetry={() => { - const returnUrl = window.location.pathname + window.location.search; + const returnUrl = returnUrlFromLocation(); + rememberOidcReturnUrl(returnUrl); void auth.signinRedirect({ state: { returnUrl } }); }} /> @@ -6314,7 +6316,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean