§9 whole-project arch+product review pass (todo §9); ran systems-architect + product-owner on the whole project (no Critical/High — a converged scaffold) and addressed the in-scope §9 customer-facing/security findings. (1) return_to deep-link login, open-redirect-safe: a gated request hit while signed out (plugin-route gate, requireSession, requireAdmin) bounces to /login?return_to=<host-relative path> via new loginRedirect(ctx) (GET/HEAD, skips /); /login bakes it into the Kratos flow — a host-relative target is wrapped through <origin>/auth/complete?return_to=<path> so the JWT mints before landing, an absolute target (§6 OAuth2 login challenge) passes to Kratos as-is; /auth/complete redirects to the requested page. (2) safeUrl()+localPath() in new pure src/safe-url.ts: safeUrl sanitises an untrusted href/src to relative-or-http(s) (else "#"), exported via plugin-api.ts (closes the contract's "planned for §9" pointer); localPath is the host-relative redirect-allowlist guard for return_to, re-checked at both /login and /auth/complete. (3) honest 503 on Ory-unreachable sign-in (views/503.ejs) instead of the misattributed catch-all 500; expired-flow 4xx still restarts. Tests-first throughout; stability-reviewer APPROVE (addressed its Medium — scoped the 503 catch so a template bug hits the 500 with a stack, not a 503). typecheck + 339 units + full scripts/ci.sh gate green. Deferred with justification: the app.ts route-table refactor (standalone change + §10 prereq), mock dashboard + public-page blessing (§10 lines 139/140), success-flash (known).

This commit is contained in:
2026-06-20 16:38:57 +02:00
parent 1118d7a9f7
commit 66ea68a91b
16 changed files with 277 additions and 48 deletions

View File

@@ -127,9 +127,10 @@ test("unknown routes serve the 404 page (a real user-facing flow, covered end-to
// The authenticated list/form flow is the §8 full E2E (full-flow.spec). Side-effect-free.
test("the reference plugin is permission-gated: anonymous → redirect to /login, hidden from the dashboard nav", async ({ page }) => {
// Don't follow the redirect — this Ory-free suite has no /login handler; assert the gate's 303 itself.
// The gate preserves the requested page as return_to (§9), so login can land back there.
const res = await page.request.get("/scheduling/shifts", { maxRedirects: 0 });
expect(res.status()).toBe(303);
expect(res.headers()["location"]).toBe("/login");
expect(res.headers()["location"]).toBe("/login?return_to=%2Fscheduling%2Fshifts");
await page.goto("/");
await expect(page.locator(".sidebar")).toContainText("People"); // dashboard nav renders