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:
@@ -28,7 +28,7 @@ pub fn form_post<U: AsRef<str>>(uri: U, cookie: &str, body: impl Into<String>) -
|
||||
|
||||
/// Form-urlencoded POST that appends `csrf_token={csrf}` automatically.
|
||||
/// Pass an empty `extra_body` when the form has no other fields
|
||||
/// (e.g. POST /web/cases/{id}/close).
|
||||
/// (e.g. POST /cases/{id}/close).
|
||||
pub fn csrf_form_post<U: AsRef<str>>(
|
||||
uri: U,
|
||||
cookie: &str,
|
||||
@@ -43,7 +43,7 @@ pub fn csrf_form_post<U: AsRef<str>>(
|
||||
form_post(uri, cookie, body)
|
||||
}
|
||||
|
||||
/// GET with a session cookie — authenticated `/web/*` pages.
|
||||
/// GET with a session cookie — authenticated `/*` pages.
|
||||
pub fn get_with_cookie<U: AsRef<str>>(uri: U, cookie: &str) -> Request<Body> {
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
|
||||
@@ -9,65 +9,65 @@
|
||||
|
||||
pub use doctate_common::{ONELINERS_PATH, UPLOAD_PATH};
|
||||
|
||||
// -- /web/* --
|
||||
// -- browser UI routes --
|
||||
|
||||
/// `POST /web/login` — form login (slug, password).
|
||||
pub const LOGIN: &str = "/web/login";
|
||||
/// `POST /web/logout` — CSRF-protected logout.
|
||||
pub const LOGOUT: &str = "/web/logout";
|
||||
/// `GET /web/cases` — case list; also triggers the lazy retention sweep.
|
||||
pub const CASES: &str = "/web/cases";
|
||||
/// `POST /web/cases/bulk` — admin bulk action (close/analyze/reset).
|
||||
pub const BULK: &str = "/web/cases/bulk";
|
||||
/// `POST /web/cases/purge-closed` — admin hard-delete of closed cases.
|
||||
pub const PURGE_CLOSED: &str = "/web/cases/purge-closed";
|
||||
/// `GET /web/events` — SSE stream (per-user scoped).
|
||||
pub const EVENTS: &str = "/web/events";
|
||||
/// `POST /login` — form login (slug, password).
|
||||
pub const LOGIN: &str = "/login";
|
||||
/// `POST /logout` — CSRF-protected logout.
|
||||
pub const LOGOUT: &str = "/logout";
|
||||
/// `GET /cases` — case list; also triggers the lazy retention sweep.
|
||||
pub const CASES: &str = "/cases";
|
||||
/// `POST /cases/bulk` — admin bulk action (close/analyze/reset).
|
||||
pub const BULK: &str = "/cases/bulk";
|
||||
/// `POST /cases/purge-closed` — admin hard-delete of closed cases.
|
||||
pub const PURGE_CLOSED: &str = "/cases/purge-closed";
|
||||
/// `GET /events` — SSE stream (per-user scoped).
|
||||
pub const EVENTS: &str = "/events";
|
||||
|
||||
// -- per-case builders --
|
||||
|
||||
/// `/web/cases/{case_id}`.
|
||||
/// `/cases/{case_id}`.
|
||||
pub fn case_detail(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}")
|
||||
format!("/cases/{case_id}")
|
||||
}
|
||||
|
||||
/// `/web/cases/{case_id}/recordings`.
|
||||
/// `/cases/{case_id}/recordings`.
|
||||
pub fn case_recordings(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/recordings")
|
||||
format!("/cases/{case_id}/recordings")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/close`.
|
||||
/// `POST /cases/{case_id}/close`.
|
||||
pub fn case_close(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/close")
|
||||
format!("/cases/{case_id}/close")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/reopen`.
|
||||
/// `POST /cases/{case_id}/reopen`.
|
||||
pub fn case_reopen(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/reopen")
|
||||
format!("/cases/{case_id}/reopen")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/analyze`.
|
||||
/// `POST /cases/{case_id}/analyze`.
|
||||
pub fn case_analyze(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/analyze")
|
||||
format!("/cases/{case_id}/analyze")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/reset` — admin-only hard-reset.
|
||||
/// `POST /cases/{case_id}/reset` — admin-only hard-reset.
|
||||
pub fn case_reset(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/reset")
|
||||
format!("/cases/{case_id}/reset")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/recordings/delete` — per-recording delete.
|
||||
/// `POST /cases/{case_id}/recordings/delete` — per-recording delete.
|
||||
pub fn recording_delete(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/recordings/delete")
|
||||
format!("/cases/{case_id}/recordings/delete")
|
||||
}
|
||||
|
||||
/// `POST /web/cases/{case_id}/oneliner` — session-authenticated manual
|
||||
/// `POST /cases/{case_id}/oneliner` — session-authenticated manual
|
||||
/// oneliner override (browser web UI).
|
||||
pub fn case_oneliner_web(case_id: &str) -> String {
|
||||
format!("/web/cases/{case_id}/oneliner")
|
||||
format!("/cases/{case_id}/oneliner")
|
||||
}
|
||||
|
||||
/// `GET /web/magic?token={token}` — magic-link consumption.
|
||||
/// `GET /magic?token={token}` — magic-link consumption.
|
||||
pub fn magic(token: &str) -> String {
|
||||
format!("/web/magic?token={token}")
|
||||
format!("/magic?token={token}")
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn extract_session_cookie(resp: &Response) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Build the `POST /web/login` request with a URL-encoded form body.
|
||||
/// Build the `POST /login` request with a URL-encoded form body.
|
||||
/// Pulled out so tests that need to *inspect* the login response
|
||||
/// (e.g. session-fixation tests) can reuse the exact wire shape.
|
||||
pub fn login_request(slug: &str, password: &str) -> Request<Body> {
|
||||
@@ -62,7 +62,7 @@ pub async fn login(app: &Router, slug: &str, password: &str) -> String {
|
||||
/// the session store. Pairs with
|
||||
/// `doctate_server::create_router_and_session_store` — the returned
|
||||
/// token is what the `CsrfForm<T>` extractor validates on every
|
||||
/// state-changing POST under `/web/`.
|
||||
/// state-changing POST under `/`.
|
||||
///
|
||||
/// Returns `(cookie_header_value, csrf_token)`.
|
||||
pub async fn login_with_csrf(
|
||||
|
||||
Reference in New Issue
Block a user