OXYGENHUB

Coverage / crates/hub/src/auth/viewer.rs 95.5%

64 of 67 lines run, 17 of 38 functions · run #532 on main · run missed

1
//! Who is asking: the session cookie resolved to an account, as an axum
2
//! extractor every page handler takes.
3
4
use super::{csrf_token, mac_equal, token_hash, SESSION_COOKIE};
5
use crate::db::{self, accounts::Account};
6
use crate::Shared;
7
use axum::extract::FromRequestParts;
8
use axum::http::request::Parts;
9
use axum::http::{header, HeaderMap};
10
use std::convert::Infallible;
11
12
#[derive(Clone, Debug)]
13
pub struct ViewerAccount {
14
    pub account: Account,
15
    /// The cookie value, which CSRF tokens are bound to.
16
    pub session: String,
17
    /// The session's record (oxygenhub#223): "this browser" in the list.
18
    pub session_id: i64,
19
    pub csrf: String,
20
    /// Unread notifications, for the header.
21
    pub unread: i64,
22
}
23
24
/// The signed-in account, if any. Never fails: anonymous is a valid viewer.
25
#[derive(Clone, Debug, Default)]
26
pub struct Viewer(pub Option<ViewerAccount>);
27
28
impl Viewer {
297322×
    pub fn account(&self) -> Option<&Account> {
307322×
        self.0.as_ref().map(|v| &v.account)
317322×
    }
32
33135×
    pub fn signed_in(&self) -> bool {
34135×
        self.0.is_some()
35135×
    }
36
371669×
    pub fn is_admin(&self) -> bool {
381669×
        self.account().is_some_and(|a| a.is_admin)
391669×
    }
40
411652×
    pub fn csrf(&self) -> &str {
421652×
        self.0.as_ref().map(|v| v.csrf.as_str()).unwrap_or("")
431652×
    }
44
45
    /// Whether a submitted CSRF token belongs to this session.
46492×
    pub fn csrf_ok(&self, token: &str) -> bool {
47492×
        self.0.as_ref().is_some_and(|v| mac_equal(&v.csrf, token))
48492×
    }
49
}
50
51
/// The value of a named cookie in the request.
521698×
pub fn cookie(headers: &HeaderMap, name: &str) -> Option<String> {
531698×
    headers.get_all(header::COOKIE).iter().filter_map(|v| v.to_str().ok()).flat_map(|line| line.split(';')).find_map(|kv| {
541219×
        let (k, v) = kv.trim().split_once('=')?;
551210×
        (k == name).then(|| v.trim().to_string())
561219×
    })
571698×
}
58
59
/// Resolve a session cookie value to its account: a live session record
60
/// (oxygenhub#223) whose salt is still the account's.
611122×
pub async fn resolve(app: &Shared, value: &str) -> Option<(ViewerAccount, db::sessions::Session)> {
621122×
    if value.is_empty() || value.len() > 128 {
630×
        return None;
641122×
    }
651122×
    let session = db::sessions::by_hash(&app.db, &token_hash(value)).await.ok()??;
661118×
    let account = db::accounts::account_by_id(&app.db, session.account_id).await.ok()??;
671118×
    if account.blocked() || !session.live(&account.session_salt, db::now_ms()) {
684×
        return None;
691114×
    }
701114×
    let csrf = csrf_token(&app.cookie_key, value);
711114×
    let unread = db::notify::unread(&app.db, account.id).await.unwrap_or(0);
721114×
    let viewer = ViewerAccount { account, session: value.to_string(), session_id: session.id, csrf, unread };
731114×
    Some((viewer, session))
741122×
}
75
76
impl FromRequestParts<Shared> for Viewer {
77
    type Rejection = Infallible;
78
791648×
    async fn from_request_parts(parts: &mut Parts, app: &Shared) -> Result<Viewer, Infallible> {
801648×
        let Some(value) = cookie(&parts.headers, SESSION_COOKIE) else { return Ok(Viewer(None)) };
811122×
        let Some((viewer, session)) = resolve(app, &value).await else { return Ok(Viewer(None)) };
821114×
        let address = crate::api::client_addr(app, &parts.headers, &parts.extensions);
831114×
        let agent: String = parts
841114×
            .headers
851114×
            .get(header::USER_AGENT)
861114×
            .and_then(|v| v.to_str().ok())
871114×
            .unwrap_or("")
881114×
            .chars()
891114×
            .filter(|c| !c.is_control())
901114×
            .take(300)
911114×
            .collect();
921114×
        let pool = app.db.clone();
931114×
        let id = viewer.account.id;
941114×
        tokio::spawn(async move {
951113×
            let _ = db::accounts::touch_seen(&pool, id).await;
961113×
            let _ = db::sessions::touch(&pool, &session, &address, &agent, db::now_ms()).await;
971113×
        });
981114×
        Ok(Viewer(Some(viewer)))
991648×
    }
100
}