LCOV - code coverage report
Current view: top level - src - session.rs (source / functions) Coverage Total Hit
Test: bliki.lcov Lines: 94.9 % 39 37
Test Date: 2025-11-27 15:46:07 Functions: 100.0 % 8 8

            Line data    Source code
       1              : //! # Session
       2              : //!
       3              : //! A session associates an cookie with a user.
       4              : //!
       5              : //! ## TODO
       6              : //! - [ ] Sign the session cookie to detect client-side tampering of the cookie.
       7              : //! - [ ] Session time-based expiration.
       8              : //!
       9              : //! [owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
      10              : 
      11              : use crate::responders::ResponseError;
      12              : use crate::user::User;
      13              : use axum::extract::{FromRef, FromRequestParts, OptionalFromRequestParts};
      14              : use axum_extra::extract::cookie::{Key, SignedCookieJar};
      15              : use bliki_session::generate_session;
      16              : use http::request::Parts;
      17              : use rusqlite::Connection;
      18              : use std::sync::{Arc, Mutex};
      19              : 
      20              : /// Maps a session_id to a user.
      21              : pub(crate) struct Session {
      22              :     /// A randomly generated unique identifier. Should have at least 64 bits of entropy.
      23              :     pub session_id: String,
      24              :     /// The user id
      25              :     user_id: i64,
      26              : }
      27              : 
      28            5 : pub(crate) fn session_new(db: &Connection, user_id: i64) -> anyhow::Result<Session> {
      29            5 :     let session_id = generate_session();
      30            5 :     let session = Session { session_id, user_id };
      31            5 :     let mut stmt = db.prepare("INSERT INTO sessions (session_id, user_id) VALUES (?, ?)")?;
      32            5 :     let _ret = stmt.insert((&session.session_id, &session.user_id))?;
      33            5 :     Ok(session)
      34            5 : }
      35              : 
      36              : impl<S> FromRequestParts<S> for User
      37              : where
      38              :     S: Send + Sync,
      39              :     Key: FromRef<S>,
      40              :     Arc<Mutex<Connection>>: FromRef<S>,
      41              : {
      42              :     type Rejection = ResponseError;
      43              : 
      44            3 :     async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
      45            3 :         let user = fetch_user(parts, state)?;
      46            3 :         if let Some(user) = user { Ok(user) } else { Err(ResponseError::unauthorized()) }
      47            3 :     }
      48              : }
      49              : 
      50              : impl<S> OptionalFromRequestParts<S> for User
      51              : where
      52              :     S: Send + Sync,
      53              :     // XXX: Why do I need add these bounds?
      54              :     Key: FromRef<S>,
      55              :     Arc<Mutex<Connection>>: FromRef<S>,
      56              : {
      57              :     type Rejection = ResponseError;
      58              : 
      59              :     /// If the session cookie, sid, is set, return the User if exists.
      60            3 :     async fn from_request_parts(
      61            3 :         parts: &mut Parts,
      62            3 :         state: &S,
      63            3 :     ) -> Result<Option<Self>, Self::Rejection> {
      64            3 :         fetch_user(parts, state)
      65            3 :     }
      66              : }
      67              : 
      68            6 : fn fetch_user<S>(parts: &mut Parts, state: &S) -> Result<Option<User>, ResponseError>
      69            6 : where
      70            6 :     Key: FromRef<S>,
      71            6 :     Arc<Mutex<Connection>>: FromRef<S>,
      72              : {
      73            6 :     let key = Key::from_ref(state);
      74            6 :     let jar = SignedCookieJar::from_headers(&parts.headers, key);
      75            6 :     let Some(cookie) = jar.get("sid") else {
      76            3 :         return Ok(None);
      77              :     };
      78            3 :     let sid = cookie.value_trimmed();
      79            3 :     let dbmux = Arc::<Mutex<Connection>>::from_ref(state);
      80            3 :     let db = dbmux.lock()?;
      81            3 :     let Ok(user_id) = user_from_session(&db, sid) else {
      82            0 :         return Ok(None);
      83              :     };
      84            3 :     let Ok(user) = crate::user::find_by_id(&db, user_id) else {
      85            0 :         return Ok(None);
      86              :     };
      87            3 :     Ok(user)
      88            6 : }
      89              : 
      90              : // TODO: Use new types
      91              : /// Returns the user_id from the session_id
      92            3 : fn user_from_session(db: &Connection, session_id: &str) -> rusqlite::Result<i64> {
      93            3 :     let mut stmt = db.prepare("SELECT user_id FROM sessions WHERE session_id = ? ")?;
      94            3 :     let user_id = stmt.query_row([session_id], |row| row.get("user_id"))?;
      95            3 :     Ok(user_id)
      96            3 : }
      97              : 
      98              : #[cfg(test)]
      99              : mod tests {
     100              :     use crate::db::setup_db;
     101              : 
     102              :     use super::*;
     103              :     use googletest::prelude::*;
     104              : 
     105              :     // TODO: Remove test once we have an 'integration test'
     106              :     #[gtest]
     107              :     fn test_session_new() -> Result<()> {
     108              :         let db = setup_db(Some("testdata/test_session_new.sql".into()));
     109              :         let session = session_new(&db, 1).or_fail()?;
     110              :         assert_eq!(session.user_id, 1);
     111              :         Ok(())
     112              :     }
     113              : }
        

Generated by: LCOV version 2.0-1