122 lines
3.3 KiB
Rust
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)
|
|
}
|