use crate::{account::hash, error::ApiResult}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[allow(unused_imports)] // Import is used in schema examples use serde_json::json; use sqlx::{Pool, Postgres, QueryBuilder}; use utoipa::ToSchema; pub const ADMIN_ROLE: &str = "ADMIN"; pub const USER_ROLE: &str = "USER"; const TABLE_NAME: &str = "users"; #[derive(Debug, Deserialize, ToSchema)] #[schema( example = json!( { "email": "user", "email": "user@example.com", "password": "changeme", "firstName": "firstname", "lastName": "lastname" } ) )] pub struct RegisterRequest { pub username: String, pub email: Option, pub password: String, #[serde(rename = "firstName")] pub first_name: String, #[serde(rename = "lastName")] pub last_name: String, } impl RegisterRequest { pub fn to_user(self) -> ApiResult { let password_hash = hash(&self.password)?; Ok(User { username: self.username, email: match self.email { Some(email) => Some(email.to_lowercase()), None => None, }, email_verified: 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, ToSchema)] #[schema( example = json!( { "username": "admin", "password": "changeme" } ) )] pub struct LoginRequest { pub username: String, pub password: String, } #[derive(Debug, Serialize, ToSchema)] pub struct UserResponse { pub username: String, pub role: String, #[serde(rename = "firstName")] pub first_name: String, #[serde(rename = "lastName")] pub last_name: String, #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, #[serde(rename = "emailVerified")] pub email_verified: bool, } impl From for UserResponse { fn from(user: User) -> Self { UserResponse { username: user.username, email_verified: user.email_verified, role: user.role, first_name: user.first_name, last_name: user.last_name, avatar: user.avatar, } } } #[derive(Debug, Deserialize, sqlx::FromRow, ToSchema)] pub struct UpdateUser { pub email: Option, pub email_verified: 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, pool: &Pool, username: &str) -> ApiResult { 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 email_verified) = self.email_verified { push_comma(&mut query_builder); query_builder.push("email_verified = "); query_builder.push_bind(email_verified); } 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); } if let Some(ref avatar) = self.avatar { push_comma(&mut query_builder); query_builder.push("avatar = "); query_builder.push_bind(avatar); } push_comma(&mut query_builder); query_builder.push("updated_at = "); query_builder.push_bind(Utc::now()); query_builder.push(" WHERE username = "); query_builder.push_bind(username); 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 username: String, pub email: Option, pub email_verified: 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(pool: &Pool, username: &str) -> Option { let user: Option = sqlx::query_as::<_, Self>(&format!( r#" SELECT * FROM {} WHERE username = $1 "#, TABLE_NAME )) .bind(username) .fetch_optional(pool) .await .unwrap_or_else(|err| { log::error!("Unable to find user '{}': {}", username, err); None }); user } pub async fn select_by_email(pool: &Pool, email: &str) -> Option { let user: Option = sqlx::query_as::<_, Self>(&format!( r#" SELECT * FROM {} WHERE email = $1 "#, TABLE_NAME )) .bind(email.to_lowercase()) .fetch_optional(pool) .await .unwrap_or_else(|err| { log::error!("Unable to find user by email '{}': {}", email, err); None }); user } #[allow(dead_code)] pub async fn count(pool: &Pool) -> i64 { sqlx::query_scalar(&format!( r#" SELECT COUNT(*) FROM {} "#, TABLE_NAME )) .fetch_one(pool) .await .unwrap_or_else(|_| 0) } pub async fn insert(&self, pool: &Pool) -> ApiResult { let user: User = sqlx::query_as::<_, Self>(&format!( r#" INSERT INTO {} ( username, email, email_verified, password_hash, role, first_name, last_name, avatar, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * "#, TABLE_NAME, )) .bind(&self.username) .bind(&self.email) .bind(&self.email_verified) .bind(&self.password_hash) .bind(&self.role) .bind(&self.first_name) .bind(&self.last_name) .bind(&self.avatar) .bind(self.created_at) .bind(self.updated_at) .fetch_one(pool) .await?; Ok(user) } }