OXYGENHUB
# 8bf3010760d7 oxygenhub / crates / hub / src / auth / viewer.rs
3.6 KiB · Rust · last changed Sessions are server-side records: random ids, expiry, listed and ended per browser 20 days ago source blame history raw
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 {
29
    pub fn account(&self) -> Option<&Account> {
30
        self.0.as_ref().map(|v| &v.account)
31
    }
32
33
    pub fn signed_in(&self) -> bool {
34
        self.0.is_some()
35
    }
36
37
    pub fn is_admin(&self) -> bool {
38
        self.account().is_some_and(|a| a.is_admin)
39
    }
40
41
    pub fn csrf(&self) -> &str {
42
        self.0.as_ref().map(|v| v.csrf.as_str()).unwrap_or("")
43
    }
44
45
    /// Whether a submitted CSRF token belongs to this session.
46
    pub fn csrf_ok(&self, token: &str) -> bool {
47
        self.0.as_ref().is_some_and(|v| mac_equal(&v.csrf, token))
48
    }
49
}
50
51
/// The value of a named cookie in the request.
52
pub fn cookie(headers: &HeaderMap, name: &str) -> Option<String> {
53
    headers.get_all(header::COOKIE).iter().filter_map(|v| v.to_str().ok()).flat_map(|line| line.split(';')).find_map(|kv| {
54
        let (k, v) = kv.trim().split_once('=')?;
55
        (k == name).then(|| v.trim().to_string())
56
    })
57
}
58
59
/// Resolve a session cookie value to its account: a live session record
60
/// (oxygenhub#223) whose salt is still the account's.
61
pub async fn resolve(app: &Shared, value: &str) -> Option<(ViewerAccount, db::sessions::Session)> {
62
    if value.is_empty() || value.len() > 128 {
63
        return None;
64
    }
65
    let session = db::sessions::by_hash(&app.db, &token_hash(value)).await.ok()??;
66
    let account = db::accounts::account_by_id(&app.db, session.account_id).await.ok()??;
67
    if account.blocked() || !session.live(&account.session_salt, db::now_ms()) {
68
        return None;
69
    }
70
    let csrf = csrf_token(&app.cookie_key, value);
71
    let unread = db::notify::unread(&app.db, account.id).await.unwrap_or(0);
72
    let viewer = ViewerAccount { account, session: value.to_string(), session_id: session.id, csrf, unread };
73
    Some((viewer, session))
74
}
75
76
impl FromRequestParts<Shared> for Viewer {
77
    type Rejection = Infallible;
78
79
    async fn from_request_parts(parts: &mut Parts, app: &Shared) -> Result<Viewer, Infallible> {
80
        let Some(value) = cookie(&parts.headers, SESSION_COOKIE) else { return Ok(Viewer(None)) };
81
        let Some((viewer, session)) = resolve(app, &value).await else { return Ok(Viewer(None)) };
82
        let address = crate::api::client_addr(app, &parts.headers, &parts.extensions);
83
        let agent: String = parts
84
            .headers
85
            .get(header::USER_AGENT)
86
            .and_then(|v| v.to_str().ok())
87
            .unwrap_or("")
88
            .chars()
89
            .filter(|c| !c.is_control())
90
            .take(300)
91
            .collect();
92
        let pool = app.db.clone();
93
        let id = viewer.account.id;
94
        tokio::spawn(async move {
95
            let _ = db::accounts::touch_seen(&pool, id).await;
96
            let _ = db::sessions::touch(&pool, &session, &address, &agent, db::now_ms()).await;
97
        });
98
        Ok(Viewer(Some(viewer)))
99
    }
100
}