Files
movies-diary/crates/presentation/src/csrf.rs
Gabriel Kaszewski 020fbb891e
Some checks failed
CI / Check / Test (push) Failing after 9m16s
feat: always-on Insights — correlation analysis over viewing habits
Conservative, live insights (ADR-0010) derived from each user's diary:
rating lift by genre (first-time watches only), genre seasonality vs a
uniform 1/12 month baseline, medium weekend-lift, and watch-time rhythm.

- domain: WatchRow model, InsightsQuery port, pure insights_analyzer with
  all honesty thresholds (sample minimums, strength cutoffs, suppressions
  for import noise), unit-tested
- adapters: thin Sqlite/Postgres fetch twins, reusing WrapUp's genre fetch
- application: get_insights use case, InsightsGroup in Deps
- composition/server: wiring through Repositories/DatabaseOutput
- presentation: GET /api/v1/insights handler, typed serde-tagged DTOs,
  openapi doc
- spa: /insights route (shadcn cards), insights feature hook, profile
  entry link, en locale strings
- server test: end-to-end seed sqlite → HTTP → rating lift + rhythm

Also records ADR-0010, defines "Insight" in the glossary, and includes
pre-existing worktree comment-stripping cleanups.
2026-10-12 19:29:54 +02:00

58 lines
1.4 KiB
Rust

use axum::{
extract::Request,
http::{HeaderValue, header},
middleware::Next,
response::Response,
};
#[derive(Clone)]
pub struct CsrfToken(pub String);
pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
headers
.get(header::COOKIE)
.and_then(|v| v.to_str().ok())
.and_then(|cookies| {
cookies
.split(';')
.find_map(|c| c.trim().strip_prefix("csrf=").map(str::to_string))
})
}
pub(crate) fn secure_flag() -> &'static str {
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
"; Secure"
} else {
""
}
}
pub async fn csrf_middleware(mut req: Request, next: Next) -> Response {
let existing = extract_from_cookie(req.headers());
let (token, needs_set) = match existing {
Some(t) => (t, false),
None => (uuid::Uuid::new_v4().to_string(), true),
};
req.extensions_mut().insert(CsrfToken(token.clone()));
let mut response = next.run(req).await;
if needs_set {
let cookie = format!(
"csrf={}; HttpOnly; Path=/; SameSite=Strict{}",
token,
secure_flag()
);
if let Ok(val) = HeaderValue::from_str(&cookie) {
response.headers_mut().append(header::SET_COOKIE, val);
}
}
response
}
pub fn mismatch(token: &CsrfToken, form_value: &str) -> bool {
token.0 != form_value || form_value.is_empty()
}