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}