yggdrasil/auth/
session.rs1#[cfg(feature = "server")]
8use chrono::{DateTime, Duration, Utc};
9#[cfg(feature = "server")]
10use uuid::Uuid;
11
12#[cfg(feature = "server")]
13pub fn generate_token() -> String {
15 Uuid::new_v4().to_string()
16}
17
18#[cfg(feature = "server")]
19pub fn hash_token(token: &str) -> String {
21 crate::utils::server::sha256_hex(token)
22}
23
24#[cfg(feature = "server")]
25pub fn default_expiry() -> DateTime<Utc> {
27 Utc::now() + Duration::days(30)
28}
29
30#[cfg(feature = "server")]
31pub fn cookie_secure() -> bool {
33 std::env::var("COOKIE_SECURE")
34 .ok()
35 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
36 .unwrap_or(false)
37}
38
39#[cfg(feature = "server")]
40pub fn session_cookie(token: &str, max_age_seconds: i32, secure: bool) -> String {
42 let secure_flag = if secure { "; Secure" } else { "" };
43 format!(
44 "session={token}; HttpOnly; Path=/; Max-Age={max_age_seconds}; SameSite=Lax{secure_flag}"
45 )
46}
47
48#[cfg(feature = "server")]
49pub fn parse_session_token(cookie_header: &str) -> Option<&str> {
51 cookie_header.split(';').map(|s| s.trim()).find_map(|pair| {
52 let mut parts = pair.splitn(2, '=');
53 let name = parts.next()?.trim();
54 let value = parts.next()?.trim();
55 if name == "session" {
56 Some(value)
57 } else {
58 None
59 }
60 })
61}
62
63#[cfg(feature = "server")]
64pub fn get_session_from_ctx() -> Option<String> {
66 use dioxus::fullstack::FullstackContext;
67
68 FullstackContext::current().and_then(|ctx| {
69 let parts = ctx.parts_mut();
70 parts
71 .headers
72 .get("cookie")
73 .and_then(|h| h.to_str().ok())
74 .and_then(parse_session_token)
75 .map(|s| s.to_string())
76 })
77}
78
79#[cfg(all(test, feature = "server"))]
80mod tests {
81 use super::*;
82 use sha2::Digest; #[test]
85 fn parse_session_found() {
86 let header = "session=abc123; path=/";
87 assert_eq!(parse_session_token(header), Some("abc123"));
88 }
89
90 #[test]
91 fn parse_session_single_cookie() {
92 assert_eq!(parse_session_token("session=token456"), Some("token456"));
93 }
94
95 #[test]
96 fn parse_session_not_found() {
97 assert_eq!(parse_session_token("other=value"), None);
98 }
99
100 #[test]
101 fn parse_session_empty_string() {
102 assert_eq!(parse_session_token(""), None);
103 }
104
105 #[test]
106 fn parse_session_multiple_cookies() {
107 let header = "theme=dark; session=my-secret; lang=en";
108 assert_eq!(parse_session_token(header), Some("my-secret"));
109 }
110
111 #[test]
112 fn parse_session_empty_value() {
113 assert_eq!(parse_session_token("session="), Some(""));
114 }
115
116 #[test]
117 fn parse_session_trailing_semicolon() {
118 assert_eq!(parse_session_token("session=abc;"), Some("abc"));
119 }
120
121 #[test]
122 fn generate_token_is_uuid() {
123 let token = generate_token();
124 assert!(uuid::Uuid::parse_str(&token).is_ok());
125 }
126
127 #[test]
128 fn default_expiry_is_future() {
129 let expiry = default_expiry();
130 assert!(expiry > chrono::Utc::now());
131 }
132
133 #[test]
134 fn default_expiry_is_about_30_days() {
135 let expiry = default_expiry();
136 let diff = expiry - chrono::Utc::now();
137 assert!(diff.num_days() >= 29 && diff.num_days() <= 31);
138 }
139
140 #[test]
141 fn hash_token_is_deterministic() {
142 let token = "test-token-123";
143 assert_eq!(hash_token(token), hash_token(token));
144 }
145
146 #[test]
147 fn hash_token_is_64_chars() {
148 let hash = hash_token("any-token");
149 assert_eq!(hash.len(), 64);
150 }
151
152 #[test]
153 fn hash_token_differs_from_input() {
154 let token = "my-secret-token";
155 assert_ne!(hash_token(token), token);
156 }
157
158 #[test]
159 fn hash_token_known_value() {
160 let hash = hash_token("hello");
161 let expected = sha2::Sha256::digest(b"hello");
162 assert_eq!(hash, hex::encode(expected));
163 }
164
165 #[test]
166 fn session_cookie_without_secure() {
167 let cookie = session_cookie("abc", 3600, false);
168 assert!(cookie.contains("session=abc"));
169 assert!(cookie.contains("HttpOnly"));
170 assert!(cookie.contains("SameSite=Lax"));
171 assert!(!cookie.contains("Secure"));
172 }
173
174 #[test]
175 fn session_cookie_with_secure() {
176 let cookie = session_cookie("abc", 3600, true);
177 assert!(cookie.contains("Secure"));
178 }
179
180 #[test]
181 fn session_cookie_logout_has_zero_max_age() {
182 let cookie = session_cookie("", 0, false);
183 assert!(cookie.contains("Max-Age=0"));
184 }
185}