From dba8ad3fdd051080d92b208402e1a6c915c183fd Mon Sep 17 00:00:00 2001 From: m Date: Fri, 8 May 2026 19:54:11 +0200 Subject: [PATCH] feat(determinator/slice-2): /projects/new return-bounce + Step 1 preselect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m's 2026-05-08 Slice 2: "Neue Akte anlegen" on the Fristenrechner now round-trips cleanly. The Step 1 link sends `?return=/tools/fristenrechner` on the way out; projects-new.ts honours the param after a successful POST and redirects back with `?project=` appended so the just-created Akte preselects itself in Step 1. Two pieces: - frontend/src/client/projects-new.ts — new sanitizeReturnUrl() rejects anything that could escape to a different origin (protocol-relative `//foo`, absolute `https://...`, non-rooted relative paths). On submit success, if a sanitized return URL exists, build the destination via URL() so existing query params on the return path stay intact and ?project= is set without clobbering, then redirect there. Falls back to /projects/{id} when no return param is present (existing behaviour preserved). - frontend/src/fristenrechner.tsx — Step 1 link gets the ?return=/tools/fristenrechner query string so the bounce-back knows where to land. Step 1 hydration from Slice 1 already handles `?project=` — fetchProjects() repopulates cachedAkten, the projectId looks up its ProjectOption record, renderStep1Summary() renders the collapsed state, Step 2 cards become visible. No client-side state coordination needed; the URL is the contract. Refs t-paliad-157 / m/paliad#15. --- frontend/src/client/projects-new.ts | 29 +++++++++++++++++++++++++++++ frontend/src/fristenrechner.tsx | 5 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/client/projects-new.ts b/frontend/src/client/projects-new.ts index a24b7f1..9decf8c 100644 --- a/frontend/src/client/projects-new.ts +++ b/frontend/src/client/projects-new.ts @@ -17,6 +17,21 @@ function $(id: string): HTMLElement { return el; } +// sanitizeReturnUrl restricts the post-create bounce-back to same-origin +// paths. Any value that could escape to a different origin (protocol- +// relative `//foo`, absolute `https://...`, or non-rooted relative +// paths) is rejected and the form falls back to /projects/{id}. m's +// 2026-05-08 Determinator Slice 2: the /tools/fristenrechner Step 1 +// "Neue Akte anlegen" link sends ?return=/tools/fristenrechner so the +// new project preselects itself when control bounces back. +function sanitizeReturnUrl(raw: string | null): string | null { + if (!raw) return null; + if (raw.startsWith("//")) return null; + if (raw.includes("://")) return null; + if (!raw.startsWith("/")) return null; + return raw; +} + function submitForm() { const form = $("project-new-form") as HTMLFormElement; const msg = $("project-new-msg") as HTMLParagraphElement; @@ -41,6 +56,20 @@ function submitForm() { return; } const p = (await resp.json()) as { id: string }; + + // Honour ?return= if it's a same-origin rooted path. The + // caller is responsible for ensuring the destination knows what + // to do with the appended ?project= param; see Slice 1's Step 1 + // hydration. + const qs = new URLSearchParams(window.location.search); + const returnUrl = sanitizeReturnUrl(qs.get("return")); + if (returnUrl) { + const dest = new URL(returnUrl, window.location.origin); + dest.searchParams.set("project", p.id); + window.location.href = dest.pathname + dest.search + dest.hash; + return; + } + window.location.href = `/projects/${p.id}`; } catch (e) { msg.textContent = String(e); diff --git a/frontend/src/fristenrechner.tsx b/frontend/src/fristenrechner.tsx index ef27c65..6aef3e7 100644 --- a/frontend/src/fristenrechner.tsx +++ b/frontend/src/fristenrechner.tsx @@ -136,7 +136,10 @@ export function renderFristenrechner(): string {
oder eine neue Akte
- + so the new Akte preselects itself in Step 1. */} + + Neue Akte anlegen