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
+1 -1
View File
@@ -340,7 +340,7 @@ impl DoctateApp {
let server_url = cfg.server_url.clone();
let api_key = cfg.api_key.clone();
let http = self.http_client.clone();
let return_to = format!("/web/cases/{case_id}");
let return_to = format!("/cases/{case_id}");
let fallback_url = join_url(&server_url, &return_to);
// Fire-and-forget on the runtime. Blocking the egui UI thread on
+7 -7
View File
@@ -35,7 +35,7 @@ pub enum MagicLinkError {
/// Request a magic-link token and build the full URL to open. The
/// `return_to` path is what the browser lands on **after** the token
/// is consumed; must start with `/web/` (server enforces this too).
/// is consumed; must start with `/` (server enforces this too).
pub async fn build_magic_url(
http: &reqwest::Client,
server_url: &str,
@@ -56,7 +56,7 @@ pub async fn build_magic_url(
let parsed: CreateResponse = resp.json().await?;
Ok(join_url(
server_url,
&format!("/web/magic?token={}", parsed.token),
&format!("/magic?token={}", parsed.token),
))
}
@@ -80,10 +80,10 @@ mod tests {
.await;
let http = reqwest::Client::new();
let url = build_magic_url(&http, &server.uri(), "test-key", "/web/cases/abc")
let url = build_magic_url(&http, &server.uri(), "test-key", "/cases/abc")
.await
.expect("magic url");
assert_eq!(url, format!("{}/web/magic?token={token}", server.uri()));
assert_eq!(url, format!("{}/magic?token={token}", server.uri()));
}
#[tokio::test]
@@ -96,7 +96,7 @@ mod tests {
.await;
let http = reqwest::Client::new();
let err = build_magic_url(&http, &server.uri(), "wrong-key", "/web/cases/abc")
let err = build_magic_url(&http, &server.uri(), "wrong-key", "/cases/abc")
.await
.expect_err("should error");
assert!(matches!(err, MagicLinkError::Status(s) if s.as_u16() == 401));
@@ -113,9 +113,9 @@ mod tests {
let http = reqwest::Client::new();
let url_with_slash = format!("{}/", server.uri());
let url = build_magic_url(&http, &url_with_slash, "k", "/web/cases/abc")
let url = build_magic_url(&http, &url_with_slash, "k", "/cases/abc")
.await
.expect("magic url");
assert!(!url.contains("//web/"), "double slash leaked: {url}");
assert!(!url.contains("//magic"), "double slash leaked: {url}");
}
}