2c6062a53e
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.
112 lines
3.5 KiB
Rust
112 lines
3.5 KiB
Rust
//! Shared vocabulary for the admin bulk-action endpoint (`POST
|
|
//! /cases/bulk`).
|
|
//!
|
|
//! The wire shape is `application/x-www-form-urlencoded` with an
|
|
//! `action=<token>` field and one or more `case_id=<uuid>` repetitions.
|
|
//! Keeping the set of accepted tokens in a single enum lets the server
|
|
//! handler match exhaustively and lets tests build request bodies
|
|
//! without string literals that can silently drift from the handler's
|
|
//! accepted values.
|
|
|
|
use std::fmt;
|
|
use std::str::FromStr;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Which operation a bulk POST applies to every selected case.
|
|
///
|
|
/// Serialized as lowercase `"close" | "analyze" | "reset"`. The server
|
|
/// handler [`BulkAction::from_str`]-parses the raw form value so an
|
|
/// unknown token produces a domain error ("Unbekannte Aktion") instead
|
|
/// of a serde/extractor error — preserving the HTTP contract across
|
|
/// refactors.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum BulkAction {
|
|
/// Mark each case closed (write `.closed` marker).
|
|
Close,
|
|
/// Queue each case for LLM analysis.
|
|
Analyze,
|
|
/// Drop derived artefacts (oneliner, document, analysis input) and
|
|
/// revert `.m4a.failed` to `.m4a` so transcription self-heal retries.
|
|
Reset,
|
|
}
|
|
|
|
impl BulkAction {
|
|
/// Lowercase on-the-wire token. Intended for test request bodies
|
|
/// and redirect targets; the server's own deserializer goes through
|
|
/// [`FromStr`] to preserve the exact error shape.
|
|
pub const fn as_str(self) -> &'static str {
|
|
match self {
|
|
BulkAction::Close => "close",
|
|
BulkAction::Analyze => "analyze",
|
|
BulkAction::Reset => "reset",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for BulkAction {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
/// Returned by [`BulkAction::from_str`] on an unknown token. Carries
|
|
/// the offending input so the caller can log it without re-capturing.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct UnknownBulkAction(pub String);
|
|
|
|
impl fmt::Display for UnknownBulkAction {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "unknown bulk action: {}", self.0)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for UnknownBulkAction {}
|
|
|
|
impl FromStr for BulkAction {
|
|
type Err = UnknownBulkAction;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"close" => Ok(BulkAction::Close),
|
|
"analyze" => Ok(BulkAction::Analyze),
|
|
"reset" => Ok(BulkAction::Reset),
|
|
other => Err(UnknownBulkAction(other.to_string())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn round_trip_lowercase_tokens() {
|
|
for v in [BulkAction::Close, BulkAction::Analyze, BulkAction::Reset] {
|
|
assert_eq!(v, v.as_str().parse().unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wire_tokens_are_stable() {
|
|
assert_eq!(BulkAction::Close.as_str(), "close");
|
|
assert_eq!(BulkAction::Analyze.as_str(), "analyze");
|
|
assert_eq!(BulkAction::Reset.as_str(), "reset");
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_token_reports_input() {
|
|
let err = "foo".parse::<BulkAction>().unwrap_err();
|
|
assert_eq!(err.0, "foo");
|
|
}
|
|
|
|
#[test]
|
|
fn serde_matches_wire() {
|
|
let json = serde_json::to_string(&BulkAction::Analyze).unwrap();
|
|
assert_eq!(json, "\"analyze\"");
|
|
let back: BulkAction = serde_json::from_str("\"reset\"").unwrap();
|
|
assert_eq!(back, BulkAction::Reset);
|
|
}
|
|
}
|