Merge: t-paliad-157 Determinator Slice 2 — /projects/new return-bounce
dba8ad3 — feat(determinator/slice-2): /projects/new now honours a
?return=<path> query param. After a successful POST it bounces to
that path with ?project=<new_uuid> appended. Sanitization rejects
protocol-relative (//foo), absolute (https://…), and non-rooted
paths to avoid open-redirect.
Step 1 of the Determinator's "Neue Akte anlegen" link sends
?return=/tools/fristenrechner. Step 1's existing URL hydration
(Slice 1) picks up the ?project= and preselects — no new server
work needed.
This commit is contained in:
@@ -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=<path> 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);
|
||||
|
||||
@@ -136,7 +136,10 @@ export function renderFristenrechner(): string {
|
||||
<div className="fristen-step1-divider">
|
||||
<span data-i18n="deadlines.step1.divider.new">oder eine neue Akte</span>
|
||||
</div>
|
||||
<a href="/projects/new" className="fristen-step1-new" id="fristen-step1-new"
|
||||
{/* return-bounce: projects-new.ts honours ?return= and
|
||||
redirects back to /tools/fristenrechner?project=<new_uuid>
|
||||
so the new Akte preselects itself in Step 1. */}
|
||||
<a href="/projects/new?return=/tools/fristenrechner" className="fristen-step1-new" id="fristen-step1-new"
|
||||
data-i18n="deadlines.step1.new.cta">
|
||||
+ Neue Akte anlegen
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user