Skip to main content

webdav_server/model/
session.rs

1use crate::model::Result;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use blake3::Hasher;
5use sqlx::SqlitePool;
6use uuid::Uuid;
7
8/// A row in the `sessions` table, representing an active authenticated session.
9///
10/// Sessions are identified by an opaque token (a UUID) that the client stores locally.
11/// The server never stores the raw token — only its BLAKE3 hash. This means even if
12/// the database is compromised, an attacker cannot reconstruct the original tokens.
13///
14/// Session lifetime is 30 days from creation. Expired sessions are lazily revoked
15/// the next time the token is presented to the [`auth_guard`] middleware.
16///
17/// [`auth_guard`]: crate::api::middleware::auth_guard
18#[derive(sqlx::FromRow)]
19pub struct Session {
20    pub token_hash: Vec<u8>,
21    pub user_id: i64,
22    pub created_at: i64,
23    pub expires_at: i64,
24}
25
26impl Session {
27    /// Creates a new entry in `Sessions` entity then returns a hashed token as String
28    ///
29    /// # Errors
30    /// - Fails with [`sqlx::sqlite::SqliteQueryResult`] if an error occurs interacting with sqlite database.
31    /// - Returns an error if system time is set earlier than [`UNIX_EPOCH`].
32    pub async fn create(pool: &SqlitePool, user_id: i64) -> Result<String> {
33        let token = Uuid::new_v4().to_string();
34
35        #[allow(clippy::as_conversions)]
36        // Happens only when you mess up with your system time
37        let created_at: i64 = SystemTime::now()
38            .duration_since(UNIX_EPOCH)?
39            .as_secs()
40            .try_into()?;
41
42        let expires_at = created_at + (30 * 24 * 60 * 60);
43
44        let token_hash = Hasher::new()
45            .update(token.as_bytes())
46            .finalize()
47            .as_bytes()
48            .to_vec();
49
50        sqlx::query!(
51            r#"
52                INSERT INTO sessions (token_hash, user_id, created_at, expires_at)
53                VALUES (?, ?, ?, ?)
54                "#,
55            token_hash,
56            user_id,
57            created_at,
58            expires_at
59        )
60        .execute(pool)
61        .await?;
62
63        Ok(token)
64    }
65
66    /// Querries the database and returns [`Option<Session>`] wrapped in [`Result`].
67    /// If any such token exists yields `Some(session)`
68    ///
69    /// # Errors
70    /// Returns [`sqlx::Error`] on failed querry to database.
71    pub async fn verify(
72        pool: &SqlitePool,
73        token_hash: &[u8],
74    ) -> Result<Option<Self>> {
75        let session = sqlx::query_as!(
76            Session,
77            r#"
78            SELECT
79                token_hash as "token_hash!",
80                user_id,
81                created_at,
82                expires_at
83            FROM sessions WHERE token_hash = ?
84        "#,
85            token_hash
86        )
87        .fetch_optional(pool)
88        .await?;
89
90        let this_moment =
91            SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
92
93        if let Some(ref sess) = session
94            && (this_moment < sess.created_at.try_into()?
95                || this_moment > sess.expires_at.try_into()?)
96        {
97            Self::revoke(pool, token_hash).await?;
98            return Ok(None);
99        }
100
101        Ok(session)
102    }
103
104    /// Removes the session entry from sessions entity.
105    ///
106    /// # Errors
107    /// Fails with [`sqlx::Error`], if querrying with database fails
108    pub async fn revoke(pool: &SqlitePool, token_hash: &[u8]) -> Result<()> {
109        let _result = sqlx::query!(
110            r#"
111            DELETE FROM sessions WHERE token_hash = ?
112        "#,
113            token_hash
114        )
115        .execute(pool)
116        .await?;
117
118        Ok(())
119    }
120}
121
122/// A fixed-size BLAKE3 hash of an opaque session token.
123///
124/// This newtype exists for two reasons:
125///
126/// 1. **Type safety:** It prevents raw byte slices from being used as cache keys by accident.
127/// 2. **Zero-copy cache key:** Using `[u8; 32]` instead of `Vec<u8>` means the cache
128///    key lives entirely on the stack with no heap allocation.
129///
130/// It implements `Hash + Eq` (required by `moka`) and `AsRef<[u8]>` for passing to
131/// database queries.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub struct TokenHash(pub [u8; 32]);
134
135impl From<blake3::Hasher> for TokenHash {
136    fn from(hasher: blake3::Hasher) -> Self {
137        Self(hasher.finalize().into())
138    }
139}
140
141impl From<&[u8; 32]> for TokenHash {
142    fn from(value: &[u8; 32]) -> Self {
143        Self(*value)
144    }
145}
146
147impl AsRef<[u8]> for TokenHash {
148    fn as_ref(&self) -> &[u8] {
149        &self.0
150    }
151}