users.rs
1 use async_trait::async_trait; 2 use chrono::{offset::Local, Duration}; 3 use loco_rs::{auth::jwt, hash, prelude::*}; 4 use serde::{Deserialize, Serialize}; 5 use serde_json::Map; 6 use uuid::Uuid; 7 8 pub use super::_entities::users::{self, ActiveModel, Entity, Model}; 9 10 pub const MAGIC_LINK_LENGTH: i8 = 32; 11 pub const MAGIC_LINK_EXPIRATION_MIN: i8 = 5; 12 13 #[derive(Debug, Deserialize, Serialize)] 14 pub struct LoginParams { 15 pub email: String, 16 pub password: String, 17 } 18 19 #[derive(Debug, Deserialize, Serialize)] 20 pub struct RegisterParams { 21 pub email: String, 22 pub password: String, 23 pub name: String, 24 } 25 26 #[derive(Debug, Validate, Deserialize)] 27 pub struct Validator { 28 #[validate(length(min = 2, message = "Name must be at least 2 characters long."))] 29 pub name: String, 30 #[validate(email(message = "invalid email"))] 31 pub email: String, 32 } 33 34 impl Validatable for ActiveModel { 35 fn validator(&self) -> Box<dyn Validate> { 36 Box::new(Validator { 37 name: self.name.as_ref().to_owned(), 38 email: self.email.as_ref().to_owned(), 39 }) 40 } 41 } 42 43 #[async_trait::async_trait] 44 impl ActiveModelBehavior for super::_entities::users::ActiveModel { 45 async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr> 46 where 47 C: ConnectionTrait, 48 { 49 self.validate()?; 50 if insert { 51 let mut this = self; 52 this.pid = ActiveValue::Set(Uuid::new_v4()); 53 this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4())); 54 Ok(this) 55 } else { 56 Ok(self) 57 } 58 } 59 } 60 61 #[async_trait] 62 impl Authenticable for Model { 63 async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> { 64 let user = users::Entity::find() 65 .filter( 66 model::query::condition() 67 .eq(users::Column::ApiKey, api_key) 68 .build(), 69 ) 70 .one(db) 71 .await?; 72 user.ok_or_else(|| ModelError::EntityNotFound) 73 } 74 75 async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> { 76 Self::find_by_pid(db, claims_key).await 77 } 78 } 79 80 impl Model { 81 /// finds a user by the provided email 82 /// 83 /// # Errors 84 /// 85 /// When could not find user by the given token or DB query error 86 pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> { 87 let user = users::Entity::find() 88 .filter( 89 model::query::condition() 90 .eq(users::Column::Email, email) 91 .build(), 92 ) 93 .one(db) 94 .await?; 95 user.ok_or_else(|| ModelError::EntityNotFound) 96 } 97 98 /// finds a user by the provided verification token 99 /// 100 /// # Errors 101 /// 102 /// When could not find user by the given token or DB query error 103 pub async fn find_by_verification_token( 104 db: &DatabaseConnection, 105 token: &str, 106 ) -> ModelResult<Self> { 107 let user = users::Entity::find() 108 .filter( 109 model::query::condition() 110 .eq(users::Column::EmailVerificationToken, token) 111 .build(), 112 ) 113 .one(db) 114 .await?; 115 user.ok_or_else(|| ModelError::EntityNotFound) 116 } 117 118 /// finds a user by the magic token and verify and token expiration 119 /// 120 /// # Errors 121 /// 122 /// When could not find user by the given token or DB query error ot token expired 123 pub async fn find_by_magic_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> { 124 let user = users::Entity::find() 125 .filter( 126 query::condition() 127 .eq(users::Column::MagicLinkToken, token) 128 .build(), 129 ) 130 .one(db) 131 .await?; 132 133 let user = user.ok_or_else(|| ModelError::EntityNotFound)?; 134 if let Some(expired_at) = user.magic_link_expiration { 135 if expired_at >= Local::now() { 136 Ok(user) 137 } else { 138 tracing::debug!( 139 user_pid = user.pid.to_string(), 140 token_expiration = expired_at.to_string(), 141 "magic token expired for the user." 142 ); 143 Err(ModelError::msg("magic token expired")) 144 } 145 } else { 146 tracing::error!( 147 user_pid = user.pid.to_string(), 148 "magic link expiration time not exists" 149 ); 150 Err(ModelError::msg("expiration token not exists")) 151 } 152 } 153 154 /// finds a user by the provided reset token 155 /// 156 /// # Errors 157 /// 158 /// When could not find user by the given token or DB query error 159 pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> { 160 let user = users::Entity::find() 161 .filter( 162 model::query::condition() 163 .eq(users::Column::ResetToken, token) 164 .build(), 165 ) 166 .one(db) 167 .await?; 168 user.ok_or_else(|| ModelError::EntityNotFound) 169 } 170 171 /// finds a user by the provided pid 172 /// 173 /// # Errors 174 /// 175 /// When could not find user or DB query error 176 pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> { 177 let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?; 178 let user = users::Entity::find() 179 .filter( 180 model::query::condition() 181 .eq(users::Column::Pid, parse_uuid) 182 .build(), 183 ) 184 .one(db) 185 .await?; 186 user.ok_or_else(|| ModelError::EntityNotFound) 187 } 188 189 /// finds a user by the provided api key 190 /// 191 /// # Errors 192 /// 193 /// When could not find user by the given token or DB query error 194 pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> { 195 let user = users::Entity::find() 196 .filter( 197 model::query::condition() 198 .eq(users::Column::ApiKey, api_key) 199 .build(), 200 ) 201 .one(db) 202 .await?; 203 user.ok_or_else(|| ModelError::EntityNotFound) 204 } 205 206 /// Verifies whether the provided plain password matches the hashed password 207 /// 208 /// # Errors 209 /// 210 /// when could not verify password 211 #[must_use] 212 pub fn verify_password(&self, password: &str) -> bool { 213 hash::verify_password(password, &self.password) 214 } 215 216 /// Asynchronously creates a user with a password and saves it to the 217 /// database. 218 /// 219 /// # Errors 220 /// 221 /// When could not save the user into the DB 222 pub async fn create_with_password( 223 db: &DatabaseConnection, 224 params: &RegisterParams, 225 ) -> ModelResult<Self> { 226 let txn = db.begin().await?; 227 228 if users::Entity::find() 229 .filter( 230 model::query::condition() 231 .eq(users::Column::Email, ¶ms.email) 232 .build(), 233 ) 234 .one(&txn) 235 .await? 236 .is_some() 237 { 238 return Err(ModelError::EntityAlreadyExists {}); 239 } 240 241 let password_hash = 242 hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?; 243 let user = users::ActiveModel { 244 email: ActiveValue::set(params.email.to_string()), 245 password: ActiveValue::set(password_hash), 246 name: ActiveValue::set(params.name.to_string()), 247 ..Default::default() 248 } 249 .insert(&txn) 250 .await?; 251 252 txn.commit().await?; 253 254 Ok(user) 255 } 256 257 /// Creates a JWT 258 /// 259 /// # Errors 260 /// 261 /// when could not convert user claims to jwt token 262 pub fn generate_jwt(&self, secret: &str, expiration: u64) -> ModelResult<String> { 263 Ok(jwt::JWT::new(secret).generate_token(expiration, self.pid.to_string(), Map::new())?) 264 } 265 } 266 267 impl ActiveModel { 268 /// Sets the email verification information for the user and 269 /// updates it in the database. 270 /// 271 /// This method is used to record the timestamp when the email verification 272 /// was sent and generate a unique verification token for the user. 273 /// 274 /// # Errors 275 /// 276 /// when has DB query error 277 pub async fn set_email_verification_sent( 278 mut self, 279 db: &DatabaseConnection, 280 ) -> ModelResult<Model> { 281 self.email_verification_sent_at = ActiveValue::set(Some(Local::now().into())); 282 self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); 283 Ok(self.update(db).await?) 284 } 285 286 /// Sets the information for a reset password request, 287 /// generates a unique reset password token, and updates it in the 288 /// database. 289 /// 290 /// This method records the timestamp when the reset password token is sent 291 /// and generates a unique token for the user. 292 /// 293 /// # Arguments 294 /// 295 /// # Errors 296 /// 297 /// when has DB query error 298 pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult<Model> { 299 self.reset_sent_at = ActiveValue::set(Some(Local::now().into())); 300 self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); 301 Ok(self.update(db).await?) 302 } 303 304 /// Records the verification time when a user verifies their 305 /// email and updates it in the database. 306 /// 307 /// This method sets the timestamp when the user successfully verifies their 308 /// email. 309 /// 310 /// # Errors 311 /// 312 /// when has DB query error 313 pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult<Model> { 314 self.email_verified_at = ActiveValue::set(Some(Local::now().into())); 315 Ok(self.update(db).await?) 316 } 317 318 /// Resets the current user password with a new password and 319 /// updates it in the database. 320 /// 321 /// This method hashes the provided password and sets it as the new password 322 /// for the user. 323 /// 324 /// # Errors 325 /// 326 /// when has DB query error or could not hashed the given password 327 pub async fn reset_password( 328 mut self, 329 db: &DatabaseConnection, 330 password: &str, 331 ) -> ModelResult<Model> { 332 self.password = 333 ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?); 334 self.reset_token = ActiveValue::Set(None); 335 self.reset_sent_at = ActiveValue::Set(None); 336 Ok(self.update(db).await?) 337 } 338 339 /// Creates a magic link token for passwordless authentication. 340 /// 341 /// Generates a random token with a specified length and sets an expiration time 342 /// for the magic link. This method is used to initiate the magic link authentication flow. 343 /// 344 /// # Errors 345 /// - Returns an error if database update fails 346 pub async fn create_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> { 347 let random_str = hash::random_string(MAGIC_LINK_LENGTH as usize); 348 let expired = Local::now() + Duration::minutes(MAGIC_LINK_EXPIRATION_MIN.into()); 349 350 self.magic_link_token = ActiveValue::set(Some(random_str)); 351 self.magic_link_expiration = ActiveValue::set(Some(expired.into())); 352 Ok(self.update(db).await?) 353 } 354 355 /// Verifies and invalidates the magic link after successful authentication. 356 /// 357 /// Clears the magic link token and expiration time after the user has 358 /// successfully authenticated using the magic link. 359 /// 360 /// # Errors 361 /// - Returns an error if database update fails 362 pub async fn clear_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> { 363 self.magic_link_token = ActiveValue::set(None); 364 self.magic_link_expiration = ActiveValue::set(None); 365 Ok(self.update(db).await?) 366 } 367 }