OXYGENHUB

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

64 of 67 lines run, 17 of 38 functions · run #473 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 {
296906×
    pub fn account(&self) -> Option<&Account> {
306906×
        self.0.as_ref().map(|v| &v.account)
316906×
    }
32
33133×
    pub fn signed_in(&self) -> bool {
34133×
        self.0.is_some()
35133×
    }
36
371580×
    pub fn is_admin(&self) -> bool {
381580×
        self.account().is_some_and(|a| a.is_admin)
391580×
    }
40
411563×
    pub fn csrf(&self) -> &str {
421563×
        self.0.as_ref().map(|v| v.csrf.as_str()).unwrap_or("")
431563×
    }
44
45
    /// Whether a submitted CSRF token belongs to this session.
46452×
    pub fn csrf_ok(&self, token: &str) -> bool {
47452×
        self.0.as_ref().is_some_and(|v| mac_equal(&v.csrf, token))
48452×
    }
49
}
50
51
/// The value of a named cookie in the request.
521610×
pub fn cookie(headers: &HeaderMap, name: &str) -> Option<String> {
531610×
    headers.get_all(header::COOKIE).iter().filter_map(|v| v.to_str().ok()).flat_map(|line| line.split(';')).find_map(|kv| {
541156×
        let (k, v) = kv.trim().split_once('=')?;
551147×
        (k == name).then(|| v.trim().to_string())
561156×
    })
571610×
}
58
59
/// Resolve a session cookie value to its account: a live session record
60
/// (oxygenhub#223) whose salt is still the account's.
611059×
pub async fn resolve(app: &Shared, value: &str) -> Option<(ViewerAccount, db::sessions::Session)> {
621059×
    if value.is_empty() || value.len() > 128 {
630×
        return None;
641059×
    }
651059×
    let session = db::sessions::by_hash(&app.db, &token_hash(value)).await.ok()??;
661055×
    let account = db::accounts::account_by_id(&app.db, session.account_id).await.ok()??;
671055×
    if account.blocked() || !session.live(&account.session_salt, db::now_ms()) {
684×
        return None;
691051×
    }
701051×
    let csrf = csrf_token(&app.cookie_key, value);
711051×
    let unread = db::notify::unread(&app.db, account.id).await.unwrap_or(0);
721051×
    let viewer = ViewerAccount { account, session: value.to_string(), session_id: session.id, csrf, unread };
731051×
    Some((viewer, session))
741059×
}
75
76
impl FromRequestParts<Shared> for Viewer {
77
    type Rejection = Infallible;
78
791560×
    async fn from_request_parts(parts: &mut Parts, app: &Shared) -> Result<Viewer, Infallible> {
801560×
        let Some(value) = cookie(&parts.headers, SESSION_COOKIE) else { return Ok(Viewer(None)) };
811059×
        let Some((viewer, session)) = resolve(app, &value).await else { return Ok(Viewer(None)) };
821051×
        let address = crate::api::client_addr(app, &parts.headers, &parts.extensions);
831051×
        let agent: String = parts
841051×
            .headers
851051×
            .get(header::USER_AGENT)
861051×
            .and_then(|v| v.to_str().ok())
871051×
            .unwrap_or("")
881051×
            .chars()
891051×
            .filter(|c| !c.is_control())
901051×
            .take(300)
911051×
            .collect();
921051×
        let pool = app.db.clone();
931051×
        let id = viewer.account.id;
941051×
        tokio::spawn(async move {
951050×
            let _ = db::accounts::touch_seen(&pool, id).await;
961050×
            let _ = db::sessions::touch(&pool, &session, &address, &agent, db::now_ms()).await;
971050×
        });
981051×
        Ok(Viewer(Some(viewer)))
991560×
    }
100
}