use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Postgres, QueryBuilder}; use uuid::Uuid; use crate::{account::hash, error::ApiResult}; use crate::db; pub const ADMIN_ROLE: &str = "ADMIN"; pub const USER_ROLE: &str = "USER"; const TABLE_NAME: &str = "users"; #[derive(Debug, Deserialize)] pub struct RegisterRequest { pub email: String, pub password: String, pub first_name: String, pub last_name: String, } impl RegisterRequest { pub fn to_user(self) -> ApiResult { let password_hash = hash(&self.password)?; Ok(User { id: Uuid::new_v4(), email: self.email.to_lowercase(), emailVerified: false, password_hash, role: USER_ROLE.to_string(), first_name: self.first_name, last_name: self.last_name, avatar: None, updated_at: Utc::now(), created_at: Utc::now(), }) } } #[derive(Debug, Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, } #[derive(Debug, Serialize)] pub struct UserResponse { pub id: Uuid, pub email_verified: bool, pub role: String, pub first_name: String, pub last_name: String, #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, } impl From for UserResponse { fn from(user: User) -> Self { UserResponse { id: user.id, email_verified: user.emailVerified, role: user.role, first_name: user.first_name, last_name: user.last_name, avatar: user.avatar, } } } #[derive(Debug, Deserialize, sqlx::FromRow)] pub struct UpdateUser { pub id: Uuid, pub email: Option, pub password: Option, pub role: Option, pub first_name: Option, pub last_name: Option, pub avatar: Option, } impl UpdateUser { pub async fn update(&self, email: &str) -> ApiResult { let pool = db::pool(); let mut query_builder: QueryBuilder = QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME)); let mut first_clause = true; let mut push_comma = |query_builder: &mut QueryBuilder| { if !first_clause { query_builder.push(", "); } else { first_clause = false; } }; if let Some(ref email) = self.email { push_comma(&mut query_builder); query_builder.push("email = "); query_builder.push_bind(email); } if let Some(ref password) = self.password { push_comma(&mut query_builder); let password_hash = hash(password)?; query_builder.push("password_hash = "); query_builder.push_bind(password_hash); } if let Some(ref role) = self.role { push_comma(&mut query_builder); query_builder.push("role = "); query_builder.push_bind(role); } if let Some(ref first_name) = self.first_name { push_comma(&mut query_builder); query_builder.push("first_name = "); query_builder.push_bind(first_name); } if let Some(ref last_name) = self.last_name { push_comma(&mut query_builder); query_builder.push("last_name = "); query_builder.push_bind(last_name); } push_comma(&mut query_builder); query_builder.push("updated_at = "); query_builder.push_bind(Utc::now()); query_builder.push(" WHERE email = "); query_builder.push_bind(email.to_string()); query_builder.push(" RETURNING *"); let query = query_builder.build_query_as::(); let user = query.fetch_one(pool).await?; Ok(user) } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct User { pub id: Uuid, pub email: String, pub emailVerified: bool, pub password_hash: String, pub role: String, pub first_name: String, pub last_name: String, pub avatar: Option, pub updated_at: DateTime, pub created_at: DateTime, } impl User { pub async fn select(email: &str) -> Option { let pool = db::pool(); let user: Option = sqlx::query_as::<_, Self>(&format!( r#" SELECT * FROM {} WHERE email = LOWER($1) "#, TABLE_NAME )) .bind(email) .fetch_optional(pool) .await .unwrap_or_else(|err| { log::error!("Unable to find user '{}': {}", email, err); None }); user } pub async fn count() -> i64 { let pool = db::pool(); sqlx::query_scalar(&format!( r#" SELECT COUNT(*) FROM {} "#, TABLE_NAME )) .fetch_one(pool) .await .unwrap_or_else(|_| 0) } pub async fn insert(&self) -> ApiResult { let pool = db::pool(); let user: User = sqlx::query_as::<_, Self>(&format!( r#" INSERT INTO {} ( email, password_hash, role, first_name, last_name, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * "#, TABLE_NAME, )) .bind(&self.email) .bind(&self.password_hash) .bind(&self.role) .bind(&self.first_name) .bind(&self.last_name) .bind(self.created_at) .bind(self.updated_at) .fetch_one(pool) .await?; Ok(user) } }