yggdrasil/src/auth/session.rs
xfy cafbddb861 refactor: replace server-only dead_code allows with cfg(feature = "server")
Replace #[allow(dead_code)] on server-only helpers with #[cfg(feature = "server")]
to make the server/WASM split explicit and avoid compiling unused server logic
into the WASM frontend.

- Gate password/session/auth/comment helpers and model parsers with server feature
- Gate related imports and tests accordingly
- Remove genuinely unused CreatePostRequest and CreateCommentRequest
- Keep #[allow(dead_code)] for true dead code (stub methods, unused public APIs)
- Use #[cfg(any(target_arch = "wasm32", test))] for THEME_KEY
- Update AGENTS.md note
2026-06-16 16:45:08 +08:00

189 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 会话 token 生成、哈希与 Cookie 处理。
//!
//! token 使用 UUID存储时使用 SHA-256 哈希,
//! Cookie 包含 HttpOnly、SameSite=Lax 与可选 Secure 标志。
//! 服务端上下文解析函数仅在 `feature = "server"` 时可用。
#[cfg(feature = "server")]
use chrono::{DateTime, Duration, Utc};
#[cfg(feature = "server")]
use sha2::{Digest, Sha256};
#[cfg(feature = "server")]
use uuid::Uuid;
#[cfg(feature = "server")]
/// 生成新的随机会话 tokenUUID 格式)。
pub fn generate_token() -> String {
Uuid::new_v4().to_string()
}
#[cfg(feature = "server")]
/// 使用 SHA-256 对 token 进行哈希,用于数据库存储。
pub fn hash_token(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
hex::encode(hasher.finalize())
}
#[cfg(feature = "server")]
/// 返回默认会话过期时间(当前时间 + 30 天)。
pub fn default_expiry() -> DateTime<Utc> {
Utc::now() + Duration::days(30)
}
#[cfg(feature = "server")]
/// 读取环境变量 `COOKIE_SECURE`,决定是否给 Cookie 添加 Secure 标志。
pub fn cookie_secure() -> bool {
std::env::var("COOKIE_SECURE")
.ok()
.map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
.unwrap_or(false)
}
#[cfg(feature = "server")]
/// 构造会话 Cookie 字符串,包含 HttpOnly、Path、Max-Age 与 SameSite。
pub fn session_cookie(token: &str, max_age_seconds: i32, secure: bool) -> String {
let secure_flag = if secure { "; Secure" } else { "" };
format!(
"session={token}; HttpOnly; Path=/; Max-Age={max_age_seconds}; SameSite=Lax{secure_flag}"
)
}
#[cfg(feature = "server")]
/// 从 `Cookie` 头中解析名为 `session` 的 token 值。
pub fn parse_session_token(cookie_header: &str) -> Option<&str> {
cookie_header.split(';').map(|s| s.trim()).find_map(|pair| {
let mut parts = pair.splitn(2, '=');
let name = parts.next()?.trim();
let value = parts.next()?.trim();
if name == "session" {
Some(value)
} else {
None
}
})
}
#[cfg(feature = "server")]
/// 从 Dioxus `FullstackContext` 中读取 Cookie 并返回会话 token。
pub fn get_session_from_ctx() -> Option<String> {
use dioxus::fullstack::FullstackContext;
FullstackContext::current().and_then(|ctx| {
let parts = ctx.parts_mut();
parts
.headers
.get("cookie")
.and_then(|h| h.to_str().ok())
.and_then(parse_session_token)
.map(|s| s.to_string())
})
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
#[test]
fn parse_session_found() {
let header = "session=abc123; path=/";
assert_eq!(parse_session_token(header), Some("abc123"));
}
#[test]
fn parse_session_single_cookie() {
assert_eq!(parse_session_token("session=token456"), Some("token456"));
}
#[test]
fn parse_session_not_found() {
assert_eq!(parse_session_token("other=value"), None);
}
#[test]
fn parse_session_empty_string() {
assert_eq!(parse_session_token(""), None);
}
#[test]
fn parse_session_multiple_cookies() {
let header = "theme=dark; session=my-secret; lang=en";
assert_eq!(parse_session_token(header), Some("my-secret"));
}
#[test]
fn parse_session_empty_value() {
assert_eq!(parse_session_token("session="), Some(""));
}
#[test]
fn parse_session_trailing_semicolon() {
assert_eq!(parse_session_token("session=abc;"), Some("abc"));
}
#[test]
fn generate_token_is_uuid() {
let token = generate_token();
assert!(uuid::Uuid::parse_str(&token).is_ok());
}
#[test]
fn default_expiry_is_future() {
let expiry = default_expiry();
assert!(expiry > chrono::Utc::now());
}
#[test]
fn default_expiry_is_about_30_days() {
let expiry = default_expiry();
let diff = expiry - chrono::Utc::now();
assert!(diff.num_days() >= 29 && diff.num_days() <= 31);
}
#[test]
fn hash_token_is_deterministic() {
let token = "test-token-123";
assert_eq!(hash_token(token), hash_token(token));
}
#[test]
fn hash_token_is_64_chars() {
let hash = hash_token("any-token");
assert_eq!(hash.len(), 64);
}
#[test]
fn hash_token_differs_from_input() {
let token = "my-secret-token";
assert_ne!(hash_token(token), token);
}
#[test]
fn hash_token_known_value() {
let hash = hash_token("hello");
let expected = sha2::Sha256::digest(b"hello");
assert_eq!(hash, hex::encode(expected));
}
#[test]
fn session_cookie_without_secure() {
let cookie = session_cookie("abc", 3600, false);
assert!(cookie.contains("session=abc"));
assert!(cookie.contains("HttpOnly"));
assert!(cookie.contains("SameSite=Lax"));
assert!(!cookie.contains("Secure"));
}
#[test]
fn session_cookie_with_secure() {
let cookie = session_cookie("abc", 3600, true);
assert!(cookie.contains("Secure"));
}
#[test]
fn session_cookie_logout_has_zero_max_age() {
let cookie = session_cookie("", 0, false);
assert!(cookie.contains("Max-Age=0"));
}
}