lightscale-admin/backend/src/auth.rs
2026-02-13 17:07:42 +09:00

122 lines
3.3 KiB
Rust

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<String> {
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<bool> {
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<String>,
ip: Option<String>,
) -> Result<String> {
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<Option<User>> {
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)
}