OXYGENHUB
# d7ce41acf22c oxygenhub / crates / hub / src / auth / viewer.rs
3.0 KiB · Rust · last changed Admins disable, enable and delete accounts 1 month 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, session_value, 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
    pub csrf: String,
18
    /// Unread notifications, for the header.
19
    pub unread: i64,
20
}
21
22
/// The signed-in account, if any. Never fails: anonymous is a valid viewer.
23
#[derive(Clone, Debug, Default)]
24
pub struct Viewer(pub Option<ViewerAccount>);
25
26
impl Viewer {
27
    pub fn account(&self) -> Option<&Account> {
28
        self.0.as_ref().map(|v| &v.account)
29
    }
30
31
    pub fn signed_in(&self) -> bool {
32
        self.0.is_some()
33
    }
34
35
    pub fn is_admin(&self) -> bool {
36
        self.account().is_some_and(|a| a.is_admin)
37
    }
38
39
    pub fn csrf(&self) -> &str {
40
        self.0.as_ref().map(|v| v.csrf.as_str()).unwrap_or("")
41
    }
42
43
    /// Whether a submitted CSRF token belongs to this session.
44
    pub fn csrf_ok(&self, token: &str) -> bool {
45
        self.0.as_ref().is_some_and(|v| mac_equal(&v.csrf, token))
46
    }
47
}
48
49
/// The value of a named cookie in the request.
50
pub fn cookie(headers: &HeaderMap, name: &str) -> Option<String> {
51
    headers.get_all(header::COOKIE).iter().filter_map(|v| v.to_str().ok()).flat_map(|line| line.split(';')).find_map(|kv| {
52
        let (k, v) = kv.trim().split_once('=')?;
53
        (k == name).then(|| v.trim().to_string())
54
    })
55
}
56
57
/// Resolve a session cookie value to its account.
58
pub async fn resolve(app: &Shared, value: &str) -> Option<ViewerAccount> {
59
    let (username, mac) = value.rsplit_once('.')?;
60
    let account = db::accounts::account_by_name(&app.db, username).await.ok()??;
61
    if account.blocked() {
62
        return None;
63
    }
64
    let expected = session_value(&app.cookie_key, &account.username, &account.session_salt);
65
    let (_, expected_mac) = expected.rsplit_once('.')?;
66
    if !mac_equal(expected_mac, mac) {
67
        return None;
68
    }
69
    let csrf = csrf_token(&app.cookie_key, &expected);
70
    let unread = db::notify::unread(&app.db, account.id).await.unwrap_or(0);
71
    Some(ViewerAccount { account, session: expected, csrf, unread })
72
}
73
74
impl FromRequestParts<Shared> for Viewer {
75
    type Rejection = Infallible;
76
77
    async fn from_request_parts(parts: &mut Parts, app: &Shared) -> Result<Viewer, Infallible> {
78
        let Some(value) = cookie(&parts.headers, SESSION_COOKIE) else { return Ok(Viewer(None)) };
79
        let viewer = resolve(app, &value).await;
80
        if let Some(v) = &viewer {
81
            let pool = app.db.clone();
82
            let id = v.account.id;
83
            tokio::spawn(async move {
84
                let _ = db::accounts::touch_seen(&pool, id).await;
85
            });
86
        }
87
        Ok(Viewer(viewer))
88
    }
89
}