/ src / models / users.rs
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, &params.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(&params.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  }