use crate::models::{Session, User}; use anyhow::{anyhow, Result}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use rand::RngCore; use sha2::{Digest, Sha256}; use sqlx::PgPool; use time::{Duration, OffsetDateTime}; use uuid::Uuid; pub const SESSION_COOKIE: &str = "ls_admin_session"; pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut rand::thread_rng()); let argon = Argon2::default(); Ok(argon .hash_password(password.as_bytes(), &salt) .map_err(|err| anyhow!("password hash failed: {}", err))? .to_string()) } pub fn verify_password(hash: &str, password: &str) -> Result { let parsed = PasswordHash::new(hash) .map_err(|err| anyhow!("invalid password hash: {}", err))?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed) .is_ok()) } pub fn generate_token() -> String { let mut random = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random); hex::encode(random) } pub fn hash_token(token: &str) -> String { let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); hex::encode(hasher.finalize()) } pub async fn create_session( pool: &PgPool, user_id: Uuid, ttl: Duration, user_agent: Option, ip: Option, ) -> Result { let token = generate_token(); let token_hash = hash_token(&token); let now = OffsetDateTime::now_utc(); let expires_at = now + ttl; sqlx::query_as::<_, Session>( r#" INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip) VALUES ($1, $2, $3, $4, $5, $5, $6, $7) RETURNING id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip "#, ) .bind(Uuid::new_v4()) .bind(user_id) .bind(token_hash) .bind(expires_at) .bind(now) .bind(user_agent) .bind(ip) .fetch_one(pool) .await?; Ok(token) } pub async fn delete_session(pool: &PgPool, token: &str) -> Result<()> { let token_hash = hash_token(token); sqlx::query("DELETE FROM sessions WHERE token_hash = $1") .bind(token_hash) .execute(pool) .await?; Ok(()) } pub async fn session_user(pool: &PgPool, token: &str) -> Result> { let token_hash = hash_token(token); let now = OffsetDateTime::now_utc(); let session = sqlx::query_as::<_, Session>( r#" SELECT id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip FROM sessions WHERE token_hash = $1 "#, ) .bind(&token_hash) .fetch_optional(pool) .await?; let Some(session) = session else { return Ok(None); }; if session.expires_at <= now { return Ok(None); } if now - Duration::minutes(5) > session.last_seen_at { sqlx::query("UPDATE sessions SET last_seen_at = $1 WHERE id = $2") .bind(now) .bind(session.id) .execute(pool) .await?; } let user = sqlx::query_as::<_, User>( r#" SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at FROM users WHERE id = $1 "#, ) .bind(session.user_id) .fetch_optional(pool) .await?; Ok(user) }