arcly_http/auth/
session.rs1use std::collections::HashMap;
26use std::sync::{Arc, RwLock};
27use std::time::{SystemTime, UNIX_EPOCH};
28
29use futures::future::BoxFuture;
30use serde::{de::DeserializeOwned, Serialize};
31
32use crate::cookie::{CookieConfig, CookieService, SameSite};
33
34pub type SessionData = HashMap<String, serde_json::Value>;
37
38pub struct Session {
40 pub id: String,
41 pub data: RwLock<SessionData>,
42 pub expires_at: u64,
43}
44
45impl Session {
46 pub fn new_empty(id: String, ttl_secs: u64) -> Arc<Self> {
47 Arc::new(Self {
48 id,
49 data: RwLock::new(HashMap::new()),
50 expires_at: unix_now() + ttl_secs,
51 })
52 }
53
54 pub fn from_data(id: String, data: SessionData, ttl_secs: u64) -> Arc<Self> {
55 Arc::new(Self {
56 id,
57 data: RwLock::new(data),
58 expires_at: unix_now() + ttl_secs,
59 })
60 }
61
62 pub fn id(&self) -> &str {
63 &self.id
64 }
65
66 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
67 let data = self.data.read().ok()?;
68 serde_json::from_value(data.get(key)?.clone()).ok()
69 }
70
71 pub fn set<T: Serialize>(&self, key: &str, value: T) {
72 if let Ok(v) = serde_json::to_value(value) {
73 if let Ok(mut data) = self.data.write() {
74 data.insert(key.to_owned(), v);
75 }
76 }
77 }
78
79 pub fn remove(&self, key: &str) {
80 if let Ok(mut data) = self.data.write() {
81 data.remove(key);
82 }
83 }
84
85 pub fn is_expired(&self) -> bool {
86 unix_now() >= self.expires_at
87 }
88
89 pub fn data_as_json(&self) -> String {
90 self.data
91 .read()
92 .ok()
93 .and_then(|d| serde_json::to_string(&*d).ok())
94 .unwrap_or_else(|| "{}".to_string())
95 }
96}
97
98pub fn unix_now() -> u64 {
99 SystemTime::now()
100 .duration_since(UNIX_EPOCH)
101 .map(|d| d.as_secs())
102 .unwrap_or(0)
103}
104
105pub trait SessionStore: Send + Sync + 'static {
109 fn load<'a>(&'a self, id: &'a str, ttl_secs: u64) -> BoxFuture<'a, Option<Arc<Session>>>;
113 fn save<'a>(&'a self, session: &'a Session, ttl_secs: u64) -> BoxFuture<'a, bool>;
114 fn delete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, bool>;
115 fn new_id(&self) -> String;
116}
117
118pub struct SessionConfig {
122 pub cookie_name: &'static str,
124 pub ttl_secs: u64,
126 pub secret: String,
128 pub secure: bool,
130 pub http_only: bool,
132 pub same_site: SameSite,
134}
135
136impl Default for SessionConfig {
137 fn default() -> Self {
138 Self {
139 cookie_name: "arcly_session",
140 ttl_secs: 3_600,
141 secret: "change-session-secret-in-production".to_string(),
142 secure: true,
143 http_only: true,
144 same_site: SameSite::Lax,
145 }
146 }
147}
148
149pub struct SessionManager {
156 store: Box<dyn SessionStore>,
157 ttl_secs: u64,
158 cookie: CookieService,
159}
160
161impl SessionManager {
162 pub fn new(store: impl SessionStore, config: SessionConfig) -> Self {
163 let cookie = CookieService::new(CookieConfig {
164 name: config.cookie_name,
165 secret: config.secret,
166 max_age_secs: config.ttl_secs,
167 secure: config.secure,
168 http_only: config.http_only,
169 same_site: config.same_site,
170 ..Default::default()
171 });
172 Self {
173 store: Box::new(store),
174 ttl_secs: config.ttl_secs,
175 cookie,
176 }
177 }
178
179 pub async fn load_from_headers(&self, headers: &axum::http::HeaderMap) -> Option<Arc<Session>> {
183 let session_id = self.cookie.extract(headers)?;
184 self.store.load(&session_id, self.ttl_secs).await
185 }
186
187 pub async fn create(&self) -> Arc<Session> {
190 Session::new_empty(self.store.new_id(), self.ttl_secs)
191 }
192
193 pub async fn save(&self, session: &Session) -> bool {
195 self.store.save(session, self.ttl_secs).await
196 }
197
198 pub async fn delete(&self, id: &str) -> bool {
200 self.store.delete(id).await
201 }
202
203 pub fn session_cookie(&self, id: &str) -> String {
205 self.cookie.bake(id)
206 }
207
208 pub fn clear_cookie(&self) -> String {
210 self.cookie.clear()
211 }
212
213 pub fn rotate_cookie_secret(&self, new_secret: &[u8], version: u64) {
216 self.cookie.rotate_secret(new_secret, version);
217 }
218}