Skip to main content

modo/auth/session/
data.rs

1//! [`Session`] — transport-agnostic session data type and axum extractor.
2//!
3//! Populated into request extensions by [`super::cookie::CookieSessionLayer`]
4//! (cookie transport) or [`super::jwt::JwtLayer`] (JWT transport). Handlers
5//! extract it the same way regardless of which transport is active.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Transport-agnostic snapshot of one authenticated session.
11///
12/// Populated into request extensions by
13/// [`CookieSessionLayer`](super::cookie::CookieSessionLayer) (cookie transport)
14/// or [`JwtLayer`](super::jwt::JwtLayer) (JWT transport). Handlers extract it
15/// the same way regardless of which transport authenticated the request:
16///
17/// ```rust,ignore
18/// use modo::auth::session::Session;
19///
20/// async fn me(session: Session) -> String {
21///     session.user_id
22/// }
23/// ```
24///
25/// The extractor returns `401 auth:session_not_found` when no row is loaded.
26/// Use `Option<Session>` for routes that serve both authenticated and
27/// unauthenticated callers.
28///
29/// `Session` is read-only — to mutate session data use
30/// [`CookieSession`](super::cookie::CookieSession) (cookie transport) or
31/// [`JwtSession`](super::jwt::JwtSession) (JWT transport).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Session {
34    /// Session ULID — unique stable identifier for this row.
35    pub id: String,
36    /// Authenticated user identifier.
37    pub user_id: String,
38    /// Client IP address recorded at login.
39    pub ip_address: String,
40    /// Raw `User-Agent` header recorded at login.
41    pub user_agent: String,
42    /// Human-readable device name derived from the user agent
43    /// (e.g. `"Chrome on macOS"`).
44    pub device_name: String,
45    /// Device category — `"desktop"`, `"mobile"`, or `"tablet"`.
46    pub device_type: String,
47    /// SHA-256 fingerprint of the browser environment, used to detect
48    /// session hijacking.
49    pub fingerprint: String,
50    /// Arbitrary JSON data attached to the session by the application.
51    pub data: serde_json::Value,
52    /// Timestamp of session creation.
53    pub created_at: DateTime<Utc>,
54    /// Timestamp of the most recent activity (updated on touch).
55    pub last_active_at: DateTime<Utc>,
56    /// Timestamp at which the session expires.
57    pub expires_at: DateTime<Utc>,
58}
59
60use super::store::SessionData;
61
62impl From<SessionData> for Session {
63    fn from(raw: SessionData) -> Self {
64        Self {
65            id: raw.id,
66            user_id: raw.user_id,
67            ip_address: raw.ip_address,
68            user_agent: raw.user_agent,
69            device_name: raw.device_name,
70            device_type: raw.device_type,
71            fingerprint: raw.fingerprint,
72            data: raw.data,
73            created_at: raw.created_at,
74            last_active_at: raw.last_active_at,
75            expires_at: raw.expires_at,
76        }
77    }
78}
79
80use axum::extract::{FromRequestParts, OptionalFromRequestParts};
81use http::request::Parts;
82
83use crate::Error;
84
85impl<S: Send + Sync> FromRequestParts<S> for Session {
86    type Rejection = Error;
87
88    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
89        parts
90            .extensions
91            .get::<Session>()
92            .cloned()
93            .ok_or_else(|| Error::unauthorized("unauthorized").with_code("auth:session_not_found"))
94    }
95}
96
97impl<S: Send + Sync> OptionalFromRequestParts<S> for Session {
98    type Rejection = Error;
99
100    async fn from_request_parts(
101        parts: &mut Parts,
102        _state: &S,
103    ) -> Result<Option<Self>, Self::Rejection> {
104        Ok(parts.extensions.get::<Session>().cloned())
105    }
106}