use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::Argon2; pub const COOKIE_NAME: &str = "fbng_session"; /// 30 days. pub const SESSION_MAX_AGE: u64 = 60 * 60 * 24 * 30; pub fn hash_password(password: &str) -> anyhow::Result { let salt = SaltString::generate(&mut rand::thread_rng()); let hash = Argon2::default() .hash_password(password.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))?; Ok(hash.to_string()) } pub fn verify_password(password: &str, hash: &str) -> bool { let Ok(parsed) = PasswordHash::new(hash) else { return false; }; Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok() } /// 32 random bytes, hex-encoded (64 chars). pub fn random_token() -> String { hex_token(32) } /// 16 random bytes, hex-encoded (32 chars). Used for public share links. pub fn share_token() -> String { hex_token(16) } fn hex_token(bytes: usize) -> String { use rand::RngCore; let mut b = [0u8; 64]; rand::thread_rng().fill_bytes(&mut b[..bytes.min(64)]); let mut s = String::with_capacity(bytes * 2); for x in b[..bytes.min(64)].iter() { s.push_str(&format!("{x:02x}")); } s } pub fn session_cookie(token: &str, https: bool) -> String { let mut c = format!( "{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}" ); if https { c.push_str("; Secure"); } c } pub fn clear_session_cookie(https: bool) -> String { let mut c = format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"); if https { c.push_str("; Secure"); } c } /// Extract the session token from the Cookie header, if present. pub fn parse_session_cookie(headers: &axum::http::HeaderMap) -> Option { let header = headers.get(axum::http::header::COOKIE)?.to_str().ok()?; for part in header.split(';') { let part = part.trim(); if let Some((k, v)) = part.split_once('=') { if k == COOKIE_NAME && !v.is_empty() { return Some(v.to_string()); } } } None }