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}