2025-10-04 20:42:49 -03:00
|
|
|
use argon2::{
|
|
|
|
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
|
|
|
Argon2,
|
|
|
|
|
};
|
2025-10-06 08:42:09 -03:00
|
|
|
use redis::Client;
|
|
|
|
|
use sqlx::{PgPool, Row}; // <-- required for .get()
|
2025-10-04 20:42:49 -03:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
pub struct AuthService {
|
|
|
|
|
pub pool: PgPool,
|
2025-10-06 08:42:09 -03:00
|
|
|
pub redis: Option<Arc<Client>>,
|
2025-10-04 20:42:49 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AuthService {
|
2025-10-06 08:42:09 -03:00
|
|
|
#[allow(clippy::new_without_default)]
|
|
|
|
|
pub fn new(pool: PgPool, redis: Option<Arc<Client>>) -> Self {
|
2025-10-04 20:42:49 -03:00
|
|
|
Self { pool, redis }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn verify_user(
|
|
|
|
|
&self,
|
|
|
|
|
username: &str,
|
|
|
|
|
password: &str,
|
|
|
|
|
) -> Result<Option<Uuid>, Box<dyn std::error::Error>> {
|
|
|
|
|
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");
|
2025-10-06 08:42:09 -03:00
|
|
|
|
|
|
|
|
if let Ok(parsed_hash) = PasswordHash::new(&password_hash) {
|
|
|
|
|
if Argon2::default()
|
|
|
|
|
.verify_password(password.as_bytes(), &parsed_hash)
|
|
|
|
|
.is_ok()
|
|
|
|
|
{
|
|
|
|
|
return Ok(Some(user_id));
|
2025-10-04 20:42:49 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-06 08:42:09 -03:00
|
|
|
|
2025-10-04 20:42:49 -03:00
|
|
|
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();
|
2025-10-06 08:42:09 -03:00
|
|
|
let password_hash = match argon2.hash_password(password.as_bytes(), &salt) {
|
|
|
|
|
Ok(ph) => ph.to_string(),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return Err(Box::new(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::Other,
|
|
|
|
|
e.to_string(),
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-10-04 20:42:49 -03:00
|
|
|
|
2025-10-06 08:42:09 -03:00
|
|
|
let row = sqlx::query(
|
2025-10-04 20:42:49 -03:00
|
|
|
"INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id",
|
|
|
|
|
)
|
|
|
|
|
.bind(username)
|
|
|
|
|
.bind(email)
|
2025-10-06 08:42:09 -03:00
|
|
|
.bind(&password_hash)
|
2025-10-04 20:42:49 -03:00
|
|
|
.fetch_one(&self.pool)
|
2025-10-06 08:42:09 -03:00
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(row.get::<Uuid, _>("id"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn delete_user_cache(
|
|
|
|
|
&self,
|
|
|
|
|
username: &str,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
if let Some(redis_client) = &self.redis {
|
|
|
|
|
let mut conn = redis_client.get_multiplexed_async_connection().await?;
|
|
|
|
|
let cache_key = format!("auth:user:{}", username);
|
|
|
|
|
|
|
|
|
|
let _: () = redis::Cmd::del(&cache_key).query_async(&mut conn).await?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_user_password(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: Uuid,
|
|
|
|
|
new_password: &str,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
|
|
|
let argon2 = Argon2::default();
|
|
|
|
|
let password_hash = match argon2.hash_password(new_password.as_bytes(), &salt) {
|
|
|
|
|
Ok(ph) => ph.to_string(),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return Err(Box::new(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::Other,
|
|
|
|
|
e.to_string(),
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sqlx::query("UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2")
|
|
|
|
|
.bind(&password_hash)
|
|
|
|
|
.bind(user_id)
|
|
|
|
|
.execute(&self.pool)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if let Some(user_row) = sqlx::query("SELECT username FROM users WHERE id = $1")
|
|
|
|
|
.bind(user_id)
|
|
|
|
|
.fetch_optional(&self.pool)
|
|
|
|
|
.await?
|
|
|
|
|
{
|
|
|
|
|
let username: String = user_row.get("username");
|
|
|
|
|
self.delete_user_cache(&username).await?;
|
|
|
|
|
}
|
2025-10-04 20:42:49 -03:00
|
|
|
|
2025-10-06 08:42:09 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
{{END_REWRITTEN_CODE}}
|
|
|
|
|
|
|
|
|
|
impl Clone for AuthService {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
pool: self.pool.clone(),
|
|
|
|
|
redis: self.redis.clone(),
|
|
|
|
|
}
|
2025-10-04 20:42:49 -03:00
|
|
|
}
|
|
|
|
|
}
|