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/// One authenticated session, regardless of transport.
11///
12/// Populated into request extensions by `CookieSessionLayer` (cookie transport)
13/// or `JwtLayer` (JWT transport). Handlers extract it directly:
14///
15/// ```rust,ignore
16/// async fn me(session: Session) -> String {
17///     session.user_id
18/// }
19/// ```
20///
21/// Returns `401 auth:session_not_found` when no row is loaded. Use
22/// `Option<Session>` for routes that serve both authenticated and
23/// unauthenticated callers.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Session {
26    pub id: String,
27    pub user_id: String,
28    pub ip_address: String,
29    pub user_agent: String,
30    pub device_name: String,
31    pub device_type: String,
32    pub fingerprint: String,
33    pub data: serde_json::Value,
34    pub created_at: DateTime<Utc>,
35    pub last_active_at: DateTime<Utc>,
36    pub expires_at: DateTime<Utc>,
37}
38
39use super::store::SessionData;
40
41impl From<SessionData> for Session {
42    fn from(raw: SessionData) -> Self {
43        Self {
44            id: raw.id,
45            user_id: raw.user_id,
46            ip_address: raw.ip_address,
47            user_agent: raw.user_agent,
48            device_name: raw.device_name,
49            device_type: raw.device_type,
50            fingerprint: raw.fingerprint,
51            data: raw.data,
52            created_at: raw.created_at,
53            last_active_at: raw.last_active_at,
54            expires_at: raw.expires_at,
55        }
56    }
57}
58
59use axum::extract::{FromRequestParts, OptionalFromRequestParts};
60use http::request::Parts;
61
62use crate::Error;
63
64impl<S: Send + Sync> FromRequestParts<S> for Session {
65    type Rejection = Error;
66
67    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
68        parts
69            .extensions
70            .get::<Session>()
71            .cloned()
72            .ok_or_else(|| Error::unauthorized("unauthorized").with_code("auth:session_not_found"))
73    }
74}
75
76impl<S: Send + Sync> OptionalFromRequestParts<S> for Session {
77    type Rejection = Error;
78
79    async fn from_request_parts(
80        parts: &mut Parts,
81        _state: &S,
82    ) -> Result<Option<Self>, Self::Rejection> {
83        Ok(parts.extensions.get::<Session>().cloned())
84    }
85}