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
+7 -7
View File
@@ -29,7 +29,7 @@ async fn web_audio_serves_file() {
let response = app
.oneshot(
Request::builder()
.uri(format!("/web/audio/dr_test/{case_id}/{filename}"))
.uri(format!("/audio/dr_test/{case_id}/{filename}"))
.body(Body::empty())
.unwrap(),
)
@@ -50,7 +50,7 @@ async fn web_audio_path_traversal_rejected() {
let response = app
.oneshot(
Request::builder()
.uri("/web/audio/..%2Fetc/550e8400-e29b-41d4-a716-446655440000/foo.m4a")
.uri("/audio/..%2Fetc/550e8400-e29b-41d4-a716-446655440000/foo.m4a")
.body(Body::empty())
.unwrap(),
)
@@ -67,7 +67,7 @@ async fn web_audio_invalid_case_id_rejected() {
let response = app
.oneshot(
Request::builder()
.uri("/web/audio/dr_test/not-a-uuid/foo.m4a")
.uri("/audio/dr_test/not-a-uuid/foo.m4a")
.body(Body::empty())
.unwrap(),
)
@@ -93,7 +93,7 @@ async fn web_audio_serves_range_as_206() {
let response = app
.oneshot(
Request::builder()
.uri(format!("/web/audio/dr_test/{case_id}/{filename}"))
.uri(format!("/audio/dr_test/{case_id}/{filename}"))
.header("Range", "bytes=10-19")
.body(Body::empty())
.unwrap(),
@@ -129,7 +129,7 @@ async fn web_audio_invalid_range_returns_416() {
let response = app
.oneshot(
Request::builder()
.uri(format!("/web/audio/dr_test/{case_id}/{filename}"))
.uri(format!("/audio/dr_test/{case_id}/{filename}"))
.header("Range", "bytes=1000-2000")
.body(Body::empty())
.unwrap(),
@@ -160,7 +160,7 @@ async fn web_audio_no_range_header_advertises_accept_ranges() {
let response = app
.oneshot(
Request::builder()
.uri(format!("/web/audio/dr_test/{case_id}/{filename}"))
.uri(format!("/audio/dr_test/{case_id}/{filename}"))
.body(Body::empty())
.unwrap(),
)
@@ -182,7 +182,7 @@ async fn web_audio_nonexistent_file_returns_404() {
let response = app
.oneshot(
Request::builder()
.uri("/web/audio/dr_test/550e8400-e29b-41d4-a716-446655440000/missing.m4a")
.uri("/audio/dr_test/550e8400-e29b-41d4-a716-446655440000/missing.m4a")
.body(Body::empty())
.unwrap(),
)