use chrono::Utc;
use crate::db::Db;
use crate::error::AuthError;
use crate::password::hash_password;
use crate::types::{Email, User, UserId, Username};
pub(crate) fn map_unique_violation(err: sqlx::Error) -> AuthError {
if let sqlx::Error::Database(ref db_err) = err {
let msg = db_err.message();
if msg.contains("UNIQUE constraint failed") {
if msg.contains("email") {
return AuthError::Conflict("email already exists".into());
}
if msg.contains("username") {
return AuthError::Conflict("username already exists".into());
}
return AuthError::Conflict(msg.to_string());
}
}
AuthError::Database(err)
}
impl Db {
pub async fn create_user(
&self,
email: Email,
password: &str,
username: Option<Username>,
) -> Result<User, AuthError> {
let id = UserId::new();
let pw_hash = hash_password(password)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
sqlx::query(
"INSERT INTO allowthem_users \
(id, email, username, password_hash, email_verified, is_active, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5)",
)
.bind(id)
.bind(&email)
.bind(&username)
.bind(&pw_hash)
.bind(&now)
.execute(self.pool())
.await
.map_err(map_unique_violation)?;
self.get_user(id).await
}
pub async fn get_user(&self, id: UserId) -> Result<User, AuthError> {
sqlx::query_as::<_, User>(
"SELECT id, email, username, NULL as password_hash, \
email_verified, is_active, created_at, updated_at \
FROM allowthem_users WHERE id = ?",
)
.bind(id)
.fetch_optional(self.pool())
.await?
.ok_or(AuthError::NotFound)
}
pub async fn get_user_by_email(&self, email: &Email) -> Result<User, AuthError> {
sqlx::query_as::<_, User>(
"SELECT id, email, username, NULL as password_hash, \
email_verified, is_active, created_at, updated_at \
FROM allowthem_users WHERE email = ?",
)
.bind(email)
.fetch_optional(self.pool())
.await?
.ok_or(AuthError::NotFound)
}
pub async fn get_user_by_username(&self, username: &Username) -> Result<User, AuthError> {
sqlx::query_as::<_, User>(
"SELECT id, email, username, NULL as password_hash, \
email_verified, is_active, created_at, updated_at \
FROM allowthem_users WHERE username = ?",
)
.bind(username)
.fetch_optional(self.pool())
.await?
.ok_or(AuthError::NotFound)
}
pub async fn find_for_login(&self, identifier: &str) -> Result<User, AuthError> {
sqlx::query_as::<_, User>(
"SELECT id, email, username, password_hash, \
email_verified, is_active, created_at, updated_at \
FROM allowthem_users WHERE email = ?1 OR username = ?1",
)
.bind(identifier)
.fetch_optional(self.pool())
.await?
.ok_or(AuthError::NotFound)
}
pub async fn update_user_email(&self, id: UserId, email: Email) -> Result<(), AuthError> {
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let result =
sqlx::query("UPDATE allowthem_users SET email = ?1, updated_at = ?2 WHERE id = ?3")
.bind(&email)
.bind(&now)
.bind(id)
.execute(self.pool())
.await
.map_err(map_unique_violation)?;
if result.rows_affected() == 0 {
return Err(AuthError::NotFound);
}
Ok(())
}
pub async fn update_user_username(
&self,
id: UserId,
username: Option<Username>,
) -> Result<(), AuthError> {
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let result =
sqlx::query("UPDATE allowthem_users SET username = ?1, updated_at = ?2 WHERE id = ?3")
.bind(&username)
.bind(&now)
.bind(id)
.execute(self.pool())
.await
.map_err(map_unique_violation)?;
if result.rows_affected() == 0 {
return Err(AuthError::NotFound);
}
Ok(())
}
pub async fn update_user_active(&self, id: UserId, is_active: bool) -> Result<(), AuthError> {
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let result =
sqlx::query("UPDATE allowthem_users SET is_active = ?1, updated_at = ?2 WHERE id = ?3")
.bind(is_active)
.bind(&now)
.bind(id)
.execute(self.pool())
.await?;
if result.rows_affected() == 0 {
return Err(AuthError::NotFound);
}
Ok(())
}
pub async fn delete_user(&self, id: UserId) -> Result<(), AuthError> {
let result = sqlx::query("DELETE FROM allowthem_users WHERE id = ?")
.bind(id)
.execute(self.pool())
.await?;
if result.rows_affected() == 0 {
return Err(AuthError::NotFound);
}
Ok(())
}
pub async fn list_users(&self) -> Result<Vec<User>, AuthError> {
sqlx::query_as::<_, User>(
"SELECT id, email, username, NULL as password_hash, \
email_verified, is_active, created_at, updated_at \
FROM allowthem_users ORDER BY created_at ASC",
)
.fetch_all(self.pool())
.await
.map_err(AuthError::Database)
}
pub async fn update_user_password(
&self,
id: UserId,
new_password: &str,
) -> Result<(), AuthError> {
let pw_hash = hash_password(new_password)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let result = sqlx::query(
"UPDATE allowthem_users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3",
)
.bind(&pw_hash)
.bind(&now)
.bind(id)
.execute(self.pool())
.await?;
if result.rows_affected() == 0 {
return Err(AuthError::NotFound);
}
Ok(())
}
}