Skip to main content

oxihuman_core/
session_token.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Session token generation and validation stub.
6
7/// Represents a session with a token and expiry.
8#[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/// Configuration for session management.
17#[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
34/// A simple in-memory session store.
35pub struct SessionStore {
36    pub config: SessionConfig,
37    sessions: Vec<Session>,
38}
39
40/// Generates a pseudo-random session token (stub — not cryptographic).
41pub 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
54/// Creates a new session for a user at the given timestamp.
55pub 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
68/// Validates a session token at the given timestamp.
69pub 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
76/// Revokes a session token, removing it from the store.
77pub 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
83/// Removes all expired sessions from the store.
84pub 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    /// Creates a new session store with the given config.
92    pub fn new(config: SessionConfig) -> Self {
93        Self {
94            config,
95            sessions: Vec::new(),
96        }
97    }
98
99    /// Returns the number of active sessions.
100    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        /* token = prefix + token_length chars */
125        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        /* expire_at = ttl_secs = 3600; now = 5000 => expired */
149        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); /* expires at 3600 */
172        create_session(&mut store, "e2", 100); /* expires at 3700 */
173        create_session(&mut store, "e3", 5000); /* expires at 8600 */
174        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}