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
+18 -19
View File
@@ -12,12 +12,14 @@ pub(crate) mod user_web;
pub(crate) mod web;
use axum::Router;
use axum::response::Redirect;
use axum::routing::{get, post, put};
use crate::AppState;
pub fn api_router() -> Router<AppState> {
Router::new()
.route("/", get(|| async { Redirect::permanent("/cases") }))
.route("/api/health", get(health::handle_health))
.route("/api/debug/whoami", get(debug::handle_whoami))
.route("/api/upload", post(upload::handle_upload))
@@ -27,50 +29,47 @@ pub fn api_router() -> Router<AppState> {
put(oneliner_override::handle_put_oneliner),
)
.route("/api/auth/magic-link", post(magic::handle_create))
.route("/web/magic", get(magic::handle_consume))
.route("/magic", get(magic::handle_consume))
.route(
"/web/login",
"/login",
get(login::handle_login_page).post(login::handle_login_submit),
)
.route("/web/logout", post(login::handle_logout))
.route("/web/cases", get(user_web::handle_my_cases))
.route("/web/cases/{case_id}", get(user_web::handle_case_page))
.route("/logout", post(login::handle_logout))
.route("/cases", get(user_web::handle_my_cases))
.route("/cases/{case_id}", get(user_web::handle_case_page))
.route(
"/web/cases/{case_id}/recordings",
"/cases/{case_id}/recordings",
get(user_web::handle_case_recordings),
)
.route(
"/web/cases/{case_id}/recordings/delete",
"/cases/{case_id}/recordings/delete",
post(case_actions::handle_delete_recording),
)
.route(
"/web/cases/{case_id}/analyze",
"/cases/{case_id}/analyze",
post(case_actions::handle_analyze_case),
)
.route(
"/web/cases/{case_id}/close",
"/cases/{case_id}/close",
post(case_actions::handle_close_case),
)
.route(
"/web/cases/{case_id}/reopen",
"/cases/{case_id}/reopen",
post(case_actions::handle_reopen_case),
)
.route(
"/web/cases/{case_id}/reset",
"/cases/{case_id}/reset",
post(case_actions::handle_reset_case),
)
.route(
"/web/cases/{case_id}/oneliner",
"/cases/{case_id}/oneliner",
post(oneliner_override::handle_web_put_oneliner),
)
.route(
"/web/cases/purge-closed",
"/cases/purge-closed",
post(case_actions::handle_purge_closed),
)
.route("/web/cases/bulk", post(bulk::handle_bulk_action))
.route(
"/web/audio/{user}/{case_id}/{filename}",
get(web::handle_audio),
)
.route("/web/events", get(events::handle_events))
.route("/cases/bulk", post(bulk::handle_bulk_action))
.route("/audio/{user}/{case_id}/{filename}", get(web::handle_audio))
.route("/events", get(events::handle_events))
}