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 {
44 pub id: String,
45 pub data: RwLock<SessionData>,
46 pub expires_at: u64,
47}
48
49impl Session {
50 pub fn new_empty(id: String, ttl_secs: u64) -> Arc<Self> {
51 Arc::new(Self {
52 id,
53 data: RwLock::new(HashMap::new()),
54 expires_at: unix_now() + ttl_secs,
55 })
56 }
57
58 pub fn from_data(id: String, data: SessionData, ttl_secs: u64) -> Arc<Self> {
59 Arc::new(Self {
60 id,
61 data: RwLock::new(data),
62 expires_at: unix_now() + ttl_secs,
63 })
64 }
65
66 pub fn id(&self) -> &str {
67 &self.id
68 }
69
70 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
71 let data = self.data.read().ok()?;
72 serde_json::from_value(data.get(key)?.clone()).ok()
73 }
74
75 pub fn set<T: Serialize>(&self, key: &str, value: T) {
76 if let Ok(v) = serde_json::to_value(value) {
77 if let Ok(mut data) = self.data.write() {
78 data.insert(key.to_owned(), v);
79 }
80 }
81 }
82
83 pub fn remove(&self, key: &str) {
84 if let Ok(mut data) = self.data.write() {
85 data.remove(key);
86 }
87 }
88
89 pub fn is_expired(&self) -> bool {
90 unix_now() >= self.expires_at
91 }
92
93 pub fn data_as_json(&self) -> String {
94 self.data
95 .read()
96 .ok()
97 .and_then(|d| serde_json::to_string(&*d).ok())
98 .unwrap_or_else(|| "{}".to_string())
99 }
100}
101
102pub fn unix_now() -> u64 {
103 SystemTime::now()
104 .duration_since(UNIX_EPOCH)
105 .map(|d| d.as_secs())
106 .unwrap_or(0)
107}
108
109pub trait SessionStore: Send + Sync + 'static {
113 fn load<'a>(&'a self, id: &'a str, ttl_secs: u64) -> BoxFuture<'a, Option<Arc<Session>>>;
117 fn save<'a>(&'a self, session: &'a Session, ttl_secs: u64) -> BoxFuture<'a, bool>;
118 fn delete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, bool>;
119 fn new_id(&self) -> String;
120}
121
122pub struct SessionConfig {
126 pub cookie_name: &'static str,
128 pub ttl_secs: u64,
130 pub secret: String,
132 pub secure: bool,
134 pub http_only: bool,
136 pub same_site: SameSite,
138}
139
140impl Default for SessionConfig {
141 fn default() -> Self {
142 Self {
143 cookie_name: "arcly_session",
144 ttl_secs: 3_600,
145 secret: "change-session-secret-in-production".to_string(),
146 secure: true,
147 http_only: true,
148 same_site: SameSite::Lax,
149 }
150 }
151}
152
153pub struct SessionManager {
160 store: Box<dyn SessionStore>,
161 ttl_secs: u64,
162 cookie: CookieService,
163}
164
165impl SessionManager {
166 pub fn new(store: impl SessionStore, config: SessionConfig) -> Self {
167 let cookie = CookieService::new(CookieConfig {
168 name: config.cookie_name,
169 secret: config.secret,
170 max_age_secs: config.ttl_secs,
171 secure: config.secure,
172 http_only: config.http_only,
173 same_site: config.same_site,
174 ..Default::default()
175 });
176 Self {
177 store: Box::new(store),
178 ttl_secs: config.ttl_secs,
179 cookie,
180 }
181 }
182
183 pub async fn load_from_headers(&self, headers: &axum::http::HeaderMap) -> Option<Arc<Session>> {
187 let session_id = self.cookie.extract(headers)?;
188 self.store.load(&session_id, self.ttl_secs).await
189 }
190
191 pub async fn create(&self) -> Arc<Session> {
194 Session::new_empty(self.store.new_id(), self.ttl_secs)
195 }
196
197 pub async fn save(&self, session: &Session) -> bool {
199 self.store.save(session, self.ttl_secs).await
200 }
201
202 pub async fn delete(&self, id: &str) -> bool {
204 self.store.delete(id).await
205 }
206
207 pub fn session_cookie(&self, id: &str) -> String {
209 self.cookie.bake(id)
210 }
211
212 pub fn clear_cookie(&self) -> String {
214 self.cookie.clear()
215 }
216
217 pub fn rotate_cookie_secret(&self, new_secret: &[u8], version: u64) {
220 self.cookie.rotate_secret(new_secret, version);
221 }
222}