oxihuman_core/
session_token.rs1#![allow(dead_code)]
4
5#[derive(Clone, Debug)]
9pub struct Session {
10 pub token: String,
11 pub user_id: String,
12 pub expires_at: u64,
13 pub created_at: u64,
14}
15
16#[derive(Clone, Debug)]
18pub struct SessionConfig {
19 pub token_length: usize,
20 pub ttl_secs: u64,
21 pub prefix: String,
22}
23
24impl Default for SessionConfig {
25 fn default() -> Self {
26 Self {
27 token_length: 32,
28 ttl_secs: 3600,
29 prefix: "sess_".into(),
30 }
31 }
32}
33
34pub struct SessionStore {
36 pub config: SessionConfig,
37 sessions: Vec<Session>,
38}
39
40pub fn generate_token(config: &SessionConfig, seed: u64) -> String {
42 let chars = b"abcdefghijklmnopqrstuvwxyz0123456789";
43 let mut result = config.prefix.clone();
44 let mut s = seed.wrapping_add(0xdeadbeef);
45 for _ in 0..config.token_length {
46 s = s
47 .wrapping_mul(6364136223846793005)
48 .wrapping_add(1442695040888963407);
49 result.push(chars[(s >> 33) as usize % chars.len()] as char);
50 }
51 result
52}
53
54pub fn create_session(store: &mut SessionStore, user_id: &str, now: u64) -> Session {
56 let seed = now.wrapping_add(user_id.len() as u64);
57 let token = generate_token(&store.config, seed);
58 let sess = Session {
59 token: token.clone(),
60 user_id: user_id.to_owned(),
61 created_at: now,
62 expires_at: now + store.config.ttl_secs,
63 };
64 store.sessions.push(sess.clone());
65 sess
66}
67
68pub fn validate_session<'a>(store: &'a SessionStore, token: &str, now: u64) -> Option<&'a Session> {
70 store
71 .sessions
72 .iter()
73 .find(|s| s.token == token && s.expires_at > now)
74}
75
76pub fn revoke_session(store: &mut SessionStore, token: &str) -> bool {
78 let before = store.sessions.len();
79 store.sessions.retain(|s| s.token != token);
80 store.sessions.len() < before
81}
82
83pub fn purge_expired(store: &mut SessionStore, now: u64) -> usize {
85 let before = store.sessions.len();
86 store.sessions.retain(|s| s.expires_at > now);
87 before.saturating_sub(store.sessions.len())
88}
89
90impl SessionStore {
91 pub fn new(config: SessionConfig) -> Self {
93 Self {
94 config,
95 sessions: Vec::new(),
96 }
97 }
98
99 pub fn active_count(&self) -> usize {
101 self.sessions.len()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn make_store() -> SessionStore {
110 SessionStore::new(SessionConfig::default())
111 }
112
113 #[test]
114 fn test_generate_token_has_prefix() {
115 let cfg = SessionConfig::default();
116 let tok = generate_token(&cfg, 42);
117 assert!(tok.starts_with("sess_"));
118 }
119
120 #[test]
121 fn test_generate_token_length() {
122 let cfg = SessionConfig::default();
123 let tok = generate_token(&cfg, 1);
124 assert_eq!(tok.len(), cfg.prefix.len() + cfg.token_length);
126 }
127
128 #[test]
129 fn test_create_session_stored() {
130 let mut store = make_store();
131 let sess = create_session(&mut store, "alice", 1000);
132 assert_eq!(store.active_count(), 1);
133 assert_eq!(sess.user_id, "alice");
134 }
135
136 #[test]
137 fn test_validate_session_success() {
138 let mut store = make_store();
139 let sess = create_session(&mut store, "bob", 1000);
140 let found = validate_session(&store, &sess.token, 1500);
141 assert!(found.is_some());
142 }
143
144 #[test]
145 fn test_validate_session_expired() {
146 let mut store = make_store();
147 let sess = create_session(&mut store, "carol", 0);
148 let found = validate_session(&store, &sess.token, 5000);
150 assert!(found.is_none());
151 }
152
153 #[test]
154 fn test_revoke_session_removes_it() {
155 let mut store = make_store();
156 let sess = create_session(&mut store, "dave", 1000);
157 let removed = revoke_session(&mut store, &sess.token);
158 assert!(removed);
159 assert_eq!(store.active_count(), 0);
160 }
161
162 #[test]
163 fn test_revoke_nonexistent_returns_false() {
164 let mut store = make_store();
165 assert!(!revoke_session(&mut store, "nonexistent"));
166 }
167
168 #[test]
169 fn test_purge_expired_removes_old_sessions() {
170 let mut store = make_store();
171 create_session(&mut store, "e1", 0); create_session(&mut store, "e2", 100); create_session(&mut store, "e3", 5000); let removed = purge_expired(&mut store, 4000);
175 assert_eq!(removed, 2);
176 assert_eq!(store.active_count(), 1);
177 }
178
179 #[test]
180 fn test_different_seeds_produce_different_tokens() {
181 let cfg = SessionConfig::default();
182 let t1 = generate_token(&cfg, 1);
183 let t2 = generate_token(&cfg, 2);
184 assert_ne!(t1, t2);
185 }
186}