//! Who is asking: the session cookie resolved to an account, as an axum //! extractor every page handler takes. use super::{csrf_token, mac_equal, session_value, SESSION_COOKIE}; use crate::db::{self, accounts::Account}; use crate::Shared; use axum::extract::FromRequestParts; use axum::http::request::Parts; use axum::http::{header, HeaderMap}; use std::convert::Infallible; #[derive(Clone, Debug)] pub struct ViewerAccount { pub account: Account, /// The cookie value, which CSRF tokens are bound to. pub session: String, pub csrf: String, /// Unread notifications, for the header. pub unread: i64, } /// The signed-in account, if any. Never fails: anonymous is a valid viewer. #[derive(Clone, Debug, Default)] pub struct Viewer(pub Option); impl Viewer { pub fn account(&self) -> Option<&Account> { self.0.as_ref().map(|v| &v.account) } pub fn signed_in(&self) -> bool { self.0.is_some() } pub fn is_admin(&self) -> bool { self.account().is_some_and(|a| a.is_admin) } pub fn csrf(&self) -> &str { self.0.as_ref().map(|v| v.csrf.as_str()).unwrap_or("") } /// Whether a submitted CSRF token belongs to this session. pub fn csrf_ok(&self, token: &str) -> bool { self.0.as_ref().is_some_and(|v| mac_equal(&v.csrf, token)) } } /// The value of a named cookie in the request. pub fn cookie(headers: &HeaderMap, name: &str) -> Option { headers.get_all(header::COOKIE).iter().filter_map(|v| v.to_str().ok()).flat_map(|line| line.split(';')).find_map(|kv| { let (k, v) = kv.trim().split_once('=')?; (k == name).then(|| v.trim().to_string()) }) } /// Resolve a session cookie value to its account. pub async fn resolve(app: &Shared, value: &str) -> Option { let (username, mac) = value.rsplit_once('.')?; let account = db::accounts::account_by_name(&app.db, username).await.ok()??; if account.banned { return None; } let expected = session_value(&app.cookie_key, &account.username, &account.session_salt); let (_, expected_mac) = expected.rsplit_once('.')?; if !mac_equal(expected_mac, mac) { return None; } let csrf = csrf_token(&app.cookie_key, &expected); let unread = db::notify::unread(&app.db, account.id).await.unwrap_or(0); Some(ViewerAccount { account, session: expected, csrf, unread }) } impl FromRequestParts for Viewer { type Rejection = Infallible; async fn from_request_parts(parts: &mut Parts, app: &Shared) -> Result { let Some(value) = cookie(&parts.headers, SESSION_COOKIE) else { return Ok(Viewer(None)) }; let viewer = resolve(app, &value).await; if let Some(v) = &viewer { let pool = app.db.clone(); let id = v.account.id; tokio::spawn(async move { let _ = db::accounts::touch_seen(&pool, id).await; }); } Ok(Viewer(viewer)) } }