91 lines
2.7 KiB
Rust
91 lines
2.7 KiB
Rust
|
|
use argon2::{
|
||
|
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||
|
|
Argon2,
|
||
|
|
};
|
||
|
|
use redis::aio::Connection as ConnectionManager;
|
||
|
|
use redis::AsyncCommands;
|
||
|
|
use sqlx::PgPool;
|
||
|
|
use std::sync::Arc;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
pub struct AuthService {
|
||
|
|
pub pool: PgPool,
|
||
|
|
pub redis: Option<Arc<ConnectionManager>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl AuthService {
|
||
|
|
pub fn new(pool: PgPool, redis: Option<Arc<ConnectionManager>>) -> Self {
|
||
|
|
Self { pool, redis }
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn verify_user(
|
||
|
|
&self,
|
||
|
|
username: &str,
|
||
|
|
password: &str,
|
||
|
|
) -> Result<Option<Uuid>, Box<dyn std::error::Error>> {
|
||
|
|
// Try Redis cache first
|
||
|
|
if let Some(redis) = &self.redis {
|
||
|
|
let cache_key = format!("auth:user:{}", username);
|
||
|
|
if let Ok(user_id_str) = redis.clone().get::<_, String>(cache_key).await {
|
||
|
|
if let Ok(user_id) = Uuid::parse_str(&user_id_str) {
|
||
|
|
return Ok(Some(user_id));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback to database
|
||
|
|
let user = sqlx::query(
|
||
|
|
"SELECT id, password_hash FROM users WHERE username = $1 AND is_active = true",
|
||
|
|
)
|
||
|
|
.bind(username)
|
||
|
|
.fetch_optional(&self.pool)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
if let Some(row) = user {
|
||
|
|
let user_id: Uuid = row.get("id");
|
||
|
|
let password_hash: String = row.get("password_hash");
|
||
|
|
let parsed_hash = PasswordHash::new(&password_hash)?;
|
||
|
|
if Argon2::default()
|
||
|
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||
|
|
.is_ok()
|
||
|
|
{
|
||
|
|
// Cache in Redis
|
||
|
|
if let Some(redis) = &self.redis {
|
||
|
|
let cache_key = format!("auth:user:{}", username);
|
||
|
|
let _: () = redis
|
||
|
|
.clone()
|
||
|
|
.set_ex(cache_key, user_id.to_string(), 3600)
|
||
|
|
.await?;
|
||
|
|
}
|
||
|
|
return Ok(Some(user_id));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(None)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn create_user(
|
||
|
|
&self,
|
||
|
|
username: &str,
|
||
|
|
email: &str,
|
||
|
|
password: &str,
|
||
|
|
) -> Result<Uuid, Box<dyn std::error::Error>> {
|
||
|
|
let salt = SaltString::generate(&mut OsRng);
|
||
|
|
let argon2 = Argon2::default();
|
||
|
|
let password_hash = argon2
|
||
|
|
.hash_password(password.as_bytes(), &salt)?
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let user_id = sqlx::query(
|
||
|
|
"INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id",
|
||
|
|
)
|
||
|
|
.bind(username)
|
||
|
|
.bind(email)
|
||
|
|
.bind(password_hash)
|
||
|
|
.fetch_one(&self.pool)
|
||
|
|
.await?
|
||
|
|
.get("id");
|
||
|
|
|
||
|
|
Ok(user_id)
|
||
|
|
}
|
||
|
|
}
|