/ backend / src / services / login_service.rs
login_service.rs
  1  use chrono::{DateTime, Duration, Utc};
  2  use sqlx::Pool;
  3  
  4  use crate::{
  5      db::{login_details_repository, new_transaction, DB},
  6      models::login_details::LoginDetails,
  7      util::{accounts_error::AccountsError, config::Config},
  8  };
  9  
 10  use super::password_service;
 11  
 12  #[derive(Debug, thiserror::Error)]
 13  pub enum LoginError {
 14      #[error("An internal error occurred")]
 15      Internal,
 16      #[error("Invalid email/password")]
 17      InvalidEmailPassword,
 18      #[error("Account is locked due to excessive incorrect login attempts")]
 19      AccountLocked,
 20      #[error("Account has not yet been activated")]
 21      AccountNotActivated,
 22  }
 23  
 24  impl From<AccountsError> for LoginError {
 25      fn from(_: AccountsError) -> Self {
 26          LoginError::Internal
 27      }
 28  }
 29  impl From<sqlx::Error> for LoginError {
 30      fn from(_: sqlx::Error) -> Self {
 31          LoginError::Internal
 32      }
 33  }
 34  
 35  pub async fn validate_login(
 36      config: &Config,
 37      db_pool: &Pool<DB>,
 38      email: String,
 39      password: String,
 40  ) -> Result<LoginDetails, LoginError> {
 41      let mut transaction = new_transaction(db_pool).await?;
 42  
 43      let login_details =
 44          match login_details_repository::get_by_email(&mut transaction, &email).await? {
 45              None => return Err(LoginError::InvalidEmailPassword),
 46              Some(l) => l,
 47          };
 48  
 49      if !password_service::verify_password(
 50          password.to_owned(),
 51          login_details.password.to_owned(),
 52          login_details.password_nonces.to_owned(),
 53          config,
 54      ) {
 55          // Password incorrect:
 56          //  - Increase the invalid password count
 57          //  - Set an appropriate account lockout depending on the number of incorrect password count.
 58          let new_invalid_password_count = login_details.incorrect_password_count + 1;
 59          let account_lockout_until = get_account_lockout(new_invalid_password_count);
 60          login_details_repository::set_account_lockout(
 61              &mut transaction,
 62              login_details.account_id,
 63              new_invalid_password_count,
 64              account_lockout_until,
 65          )
 66          .await?;
 67  
 68          transaction.commit().await?;
 69          return Err(LoginError::InvalidEmailPassword);
 70      }
 71  
 72      let now = Utc::now();
 73      if let Some(locked_until) = login_details.account_locked_until {
 74          if locked_until > now {
 75              return Err(LoginError::AccountLocked);
 76          }
 77      }
 78  
 79      // If we reach here, we now accept the user as authorized with the account
 80  
 81      if login_details.activated_at.is_none() {
 82          return Err(LoginError::AccountNotActivated);
 83      }
 84  
 85      // Reset account lock on successful login
 86      login_details_repository::set_account_lockout(
 87          &mut transaction,
 88          login_details.account_id,
 89          0,
 90          Option::None,
 91      )
 92      .await?;
 93  
 94      transaction.commit().await?;
 95      Ok(login_details)
 96  }
 97  
 98  /// Generate a time until which the account will be locked
 99  /// Based on the given number of incorrect password count.
100  ///
101  /// Returns an option which if Some(v) will contain the datetime until which the account should be locked
102  /// Or None if the account shouldn't be locked.
103  fn get_account_lockout(invalid_password_count: i32) -> Option<DateTime<Utc>> {
104      let lockout_duration = match invalid_password_count {
105          // We don't need to lock the account on the first couple of failures.
106          0 => return None,
107          1 => Duration::seconds(1),
108          2 => Duration::seconds(5),
109          3 => Duration::minutes(2),
110          4 => Duration::minutes(15),
111          5 => Duration::minutes(30),
112          6 => Duration::hours(2),
113          7 => Duration::days(1),
114          8 => Duration::weeks(1),
115          9 => Duration::weeks(2),
116          _ => Duration::days(365),
117      };
118  
119      let now = Utc::now();
120      let locked_until = now
121          .checked_add_signed(lockout_duration)
122          .unwrap_or(DateTime::<Utc>::MAX_UTC);
123      Some(locked_until)
124  }