refactor: drop /web/ URL prefix from browser routes

The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
This commit is contained in:
2026-05-04 18:36:10 +02:00
parent 3d67cbc1c8
commit 2c6062a53e
43 changed files with 313 additions and 266 deletions
+20 -20
View File
@@ -330,13 +330,13 @@
</head>
<body>
<header>
<div><a class="back" href="/web/cases">&larr; Fälle</a></div>
<div><a class="back" href="/cases">&larr; Fälle</a></div>
<div class="header-right">
{% if is_admin %}<label class="admin-toggle"
><input type="checkbox" id="admin-view-toggle" /> Admin
view</label
>{% endif %}
<form method="post" action="/web/logout">
<form method="post" action="/logout">
{% call csrf::field(csrf_token) %}
<button type="submit">Logout</button>
</form>
@@ -362,7 +362,7 @@
<form
class="delete-form"
method="post"
action="/web/cases/{{ case_id }}/reopen"
action="/cases/{{ case_id }}/reopen"
>
{% call csrf::field(csrf_token) %}
<button
@@ -393,7 +393,7 @@
<form
class="delete-form"
method="post"
action="/web/cases/{{ case_id }}/close"
action="/cases/{{ case_id }}/close"
>
{% call csrf::field(csrf_token) %}
<button
@@ -435,15 +435,15 @@
<div class="actions">
{% if can_analyze && !has_document %}
<form method="post" action="/web/cases/{{ case_id }}/analyze">
<form method="post" action="/cases/{{ case_id }}/analyze">
{% call csrf::field(csrf_token) %}
<input type="hidden" name="return_to" value="/web/cases/{{ case_id }}">
<input type="hidden" name="return_to" value="/cases/{{ case_id }}">
<button type="submit">Analysieren</button>
</form>
{% if is_admin %}
<form class="admin-only" method="post" action="/web/cases/{{ case_id }}/analyze">
<form class="admin-only" method="post" action="/cases/{{ case_id }}/analyze">
{% call csrf::field(csrf_token) %}
<input type="hidden" name="return_to" value="/web/cases/{{ case_id }}">
<input type="hidden" name="return_to" value="/cases/{{ case_id }}">
{% for b in backends %}
<button type="submit" name="backend" value="{{ b.id }}"
title="Analysieren mit {{ b.label }}">{{ b.label }}</button>
@@ -456,10 +456,10 @@
<form
class="admin-only"
method="post"
action="/web/cases/{{ case_id }}/reset"
action="/cases/{{ case_id }}/reset"
>
{% call csrf::field(csrf_token) %}
<input type="hidden" name="return_to" value="/web/cases/{{ case_id }}">
<input type="hidden" name="return_to" value="/cases/{{ case_id }}">
<button type="submit">Reset</button>
</form>
<button
@@ -478,9 +478,9 @@
<div id="doc-content" class="doc-content">
{% if can_analyze %}
<div class="doc-actions-top">
<form method="post" action="/web/cases/{{ case_id }}/analyze">
<form method="post" action="/cases/{{ case_id }}/analyze">
{% call csrf::field(csrf_token) %}
<input type="hidden" name="return_to" value="/web/cases/{{ case_id }}">
<input type="hidden" name="return_to" value="/cases/{{ case_id }}">
<button
type="submit"
class="copy-btn"
@@ -506,9 +506,9 @@
</button>
</form>
{% if is_admin %}
<form class="admin-only" method="post" action="/web/cases/{{ case_id }}/analyze">
<form class="admin-only" method="post" action="/cases/{{ case_id }}/analyze">
{% call csrf::field(csrf_token) %}
<input type="hidden" name="return_to" value="/web/cases/{{ case_id }}">
<input type="hidden" name="return_to" value="/cases/{{ case_id }}">
{% for b in backends %}
<button
type="submit"
@@ -625,13 +625,13 @@
</details>
<form
method="post"
action="/web/cases/{{ case_id }}/analyze"
action="/cases/{{ case_id }}/analyze"
>
{% call csrf::field(csrf_token) %}
<input
type="hidden"
name="return_to"
value="/web/cases/{{ case_id }}"
value="/cases/{{ case_id }}"
/>
<button type="submit">Erneut versuchen</button>
</form>
@@ -639,13 +639,13 @@
<form
class="admin-only"
method="post"
action="/web/cases/{{ case_id }}/analyze"
action="/cases/{{ case_id }}/analyze"
>
{% call csrf::field(csrf_token) %}
<input
type="hidden"
name="return_to"
value="/web/cases/{{ case_id }}"
value="/cases/{{ case_id }}"
/>
{% for b in backends %}
<button type="submit" name="backend" value="{{ b.id }}">
@@ -669,7 +669,7 @@
<div>
<a
class="recordings-link"
href="/web/cases/{{ case_id }}/recordings"
href="/cases/{{ case_id }}/recordings"
>&rarr; Aufnahmen anzeigen ({{ recordings_count }})</a
>
</div>
@@ -730,7 +730,7 @@
clearTimeout(timer);
timer = setTimeout(() => location.reload(), 300);
};
const es = new EventSource("/web/events");
const es = new EventSource("/events");
// Release the socket-pool slot on navigation. Without this, Chrome
// keeps the SSE connection "draining" after unload; six rapid nav
// cycles exhaust the 6-per-origin pool and stall further requests.