From 951d10eb0570dc915f27437813358056810251ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:24:45 +0000 Subject: [PATCH 1/3] feat: remember login return path before OIDC redirect (v2.12.19) Unauthenticated Log in now stores a validated same-origin return path (ADR 0119 / 0109) before signinRedirect, so a shared /?post= link still opens that post after callback. Tenant admin settings stay off the signed-out login shell so tsc -b type-checks. Independent of leftover-map PRs. Do not invent a leftover score or a theta. Synthetic fixtures only. --- AGENTS.md | 5 +++ ARCHITECTURE.md | 4 +- .../2.12.19-oidc-login-return-remember.md | 6 +++ CHANGELOG.md | 10 +++++ .../adr/0109-oidc-deep-link-state-recovery.md | 1 + .../0119-oidc-login-remembers-return-path.md | 39 +++++++++++++++++++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 26 +++++++++++++ frontend/src/App.tsx | 4 +- pyproject.toml | 2 +- uv.lock | 2 +- 11 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.d/2.12.19-oidc-login-return-remember.md create mode 100644 docs/adr/0119-oidc-login-remembers-return-path.md diff --git a/AGENTS.md b/AGENTS.md index 1728f9e61..320a86d34 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 / 0119) 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 d0280ff97..aad3f0a96 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..11aeb7b3d --- /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 0119 / 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 c8ed1a099..75f794db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ All notable changes to this project are documented here. Format follows environment, so local OIDC and synthetic-data workflows resolve the same pinned dependencies as CI. +## [2.12.19] - 2026-08-24 + +### Fixed + +- Log in remembers a validated same-origin return path (ADR 0119) 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..0c4f87427 100644 --- a/docs/adr/0109-oidc-deep-link-state-recovery.md +++ b/docs/adr/0109-oidc-deep-link-state-recovery.md @@ -3,6 +3,7 @@ - Status: Accepted - Date: 2026-08-20 - Depends on: [0069](0069-member-locale-preference.md), [0028](0028-keyverse-oidc-provider.md) +- Amended by: [0119](0119-oidc-login-remembers-return-path.md) ## Context diff --git a/docs/adr/0119-oidc-login-remembers-return-path.md b/docs/adr/0119-oidc-login-remembers-return-path.md new file mode 100644 index 000000000..92bfa3f0f --- /dev/null +++ b/docs/adr/0119-oidc-login-remembers-return-path.md @@ -0,0 +1,39 @@ +# ADR 0119 — Login remembers a safe return path and does not mount admin settings + +**Decision status:** Accepted +**Date:** 2026-08-24 +**Amends:** [ADR 0109](0109-oidc-deep-link-state-recovery.md) + +## Context + +ADR 0109 recovers `/?post=` across OIDC callback contexts by persisting a +validated same-origin path before redirect and restoring it on callback. +The unauthenticated login control later stopped calling +`rememberOidcReturnUrl` / `returnUrlFromLocation`, so the callback helpers +had nothing to restore when provider state was omitted. The same login shell +also type-checked an `AdminPanel` with an undefined access token, which +broke the frontend production build (`tsc -b`) and would have asked a +signed-out buyer to save tenant settings. + +## Decision + +- The unauthenticated **Log in** control takes `returnUrl` from + `returnUrlFromLocation()`, calls `rememberOidcReturnUrl(returnUrl)`, then + starts `signinRedirect({ state: { returnUrl } })`. Raw + `pathname + search` concatenation is not a return URL. +- Tenant admin settings mount only after authentication has produced an + access token. The signed-out login shell does not render `AdminPanel`. +- Do not invent a leftover score, a theta, or a tenant name. Synthetic + fixtures only. + +## Consequences + +Opening a shared post link, logging in, and landing on that post works when +the provider omits application state. `pnpm run build` type-checks the login +shell. Independent of leftover-map PRs (#481, #485, #518, #519, #521, #522). + +## References + +Miles, A., & Bechhofer, S. (Eds.). (2009). *SKOS simple knowledge +organization system reference*. World Wide Web Consortium. +https://www.w3.org/TR/skos-reference/ diff --git a/frontend/package.json b/frontend/package.json index e2e996bbe..78ce628e2 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 7462abd2c..7b732d192 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -31,6 +31,11 @@ afterEach(() => { }); describe("App, unauthenticated", () => { + beforeEach(() => { + window.sessionStorage.clear(); + window.localStorage.clear(); + }); + it("shows a login button that starts the real OIDC redirect", async () => { render(); const button = screen.getByRole("button", { name: /log in/i }); @@ -42,6 +47,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(); + }); }); function jsonResponse(body: unknown): Response { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..1b5b351ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
- {destination === "admin" ? : null}