login_app 0.1.5

A generic login module for web apps
Documentation
// login.rs
// login related functions are implemented here

use ::log::*; // external crate for macros such as debug!, info!, so on
use bcrypt::verify;
use std::{thread, time};
use std::collections::HashMap;

use crate::db;
use crate::utils;
use crate::messages::Msg;

#[derive(Debug)]
pub struct Login {  
    email: String,
    password: String,
    status: String,
}

impl Login {
    
    // constructor
    pub fn authenticate(email: &str, password: &str) -> Result<Msg, Msg> {
        debug!("Inside login::authenticate({:?}, {:?})", email, password);
        let login = Login { 
                        email: email.to_string(), 
                        password: password.to_string(), 
                        status: String::new()
        };
        login.process()
    }

    // getters and setters
    pub fn email(&self) -> String {
        self.email.clone()
    }
    pub fn password(&self) -> String {
        self.password.clone()
    }

    // Operations
    fn process(&self) -> Result<Msg, Msg> {
        debug!("Inside login.process()...");
        let user = db::get_user_for(&self.email, &Msg::Active.description()).unwrap_or(db::User::default());
        debug!("User received: {:?}", &user);
        if user.email().is_empty() {
            debug!("User is empty...returning InvalidCredentials");
            return Err(Msg::InvalidCredentials);
        }
        debug!("User is: {:?}", &user);
        debug!("Verify between self password: {:?} and user's {:?}", &self.password(), &user.password());
        if verify(&self.password(), &user.password()).unwrap() { 
            debug!("Login Success");
            Ok(Msg::LoginSucceeded) 
        } else {
            error!("Invalid password");
            Err(Msg::InvalidCredentials)
        }
    }

    pub fn forgot_password(email: &str) -> Result<Msg, Msg> {
        debug!("forgot_password({:?})............................<<", email);
        if db::email_record_count(email) < 1 {
            return Err(Msg::EmailNotExist);
        }
        let mut user = db::get_user_for_email(email)?;
        let result = Msg::key_for(&user.status());
        debug!("Msg key: {:?}", &result);

        match result {
            Msg::Cancelled => Err(Msg::Cancelled),
            Msg::Dormant => Err(Msg::Dormant),
            Msg::ConfirmationPending => Err(Msg::ConfirmationPending),
            _ => {
                user.set_status(&Msg::ForgotPasswordPending.description());
                let token = utils::generate_token();
                let row_count = user.update_with_token(&token);
                debug!("{:?} row(s) updated in user with token for forgot password", &row_count);
                Self::send_forgot_password_email(&user.email(), &token);
                if row_count == 1 {
                    Ok(Msg::ForgotPasswordProcessed)
                } else {
                    Err(Msg::ForgotPasswordFailed)
                }
            }
        }
    }

    pub fn forgot_password_expired(email: &str) -> Result<Msg, Msg> {
        let mut user = db::get_user_for(email, &Msg::ForgotPasswordPending.description()).unwrap_or(db::User::default());
        if user.email().is_empty() { return Err(Msg::ForgotPasswordFailed) }
        user.set_status(&Msg::Active.description());
        let row_count = user.clear_token();
        if row_count == 1 {
            Ok(Msg::ForgotPasswordTokenExpired)
        } else {
            Err(Msg::ForgotPasswordFailed)
        }
    }

    fn send_forgot_password_email(email_to: &str, token: &str) {
        let email_enabled: bool = super::app_config("email_enabled") == "true".to_string();
        if !email_enabled { return (); }
        let email_from = super::app_config("email_from");
        let message_body = format!("Forgot Password Token: {}.\n\nPlease copy this token into Forgot Password Confirmation window to complete reset process", &token);
        let subject = "Login-app: Forgot Password regarding...";
        let email = utils::Email::new(&email_from, &email_to, &subject, &message_body);
        email.send();
    }

    pub fn reset_password(map: &HashMap<String, String>, email: String) -> Result<Msg, Vec<Msg>> {
        let new_pwd = map.get("password").unwrap();
        let repeat_pwd = map.get("repeat-password").unwrap();
        if new_pwd != repeat_pwd {
            return Err(vec![Msg::PasswordsDoNotMatch])
        }
        let messages = utils::Validator::validate_password(new_pwd.to_string());
        if !messages.is_empty() { return Err(messages); }

        let mut user = db::get_user_for(&email, &Msg::ForgotPasswordPending.description()).unwrap_or(db::User::default());
        if user.email().is_empty() { return Err( vec![Msg::ResetPasswordFailed] ) }        

        user.set_status(&Msg::Active.description()); 
        let hashed_pwd = utils::hash(&new_pwd);
        user.set_password(&hashed_pwd);
        let count = user.update_for_password();

        debug!("{:?} row(s) affected for user.update_for_password", &count);
        if count != 1 { return Err( vec![Msg::ResetPasswordFailed] ) };

        Ok(Msg::ResetPasswordProcessed)
    }

    // await user confirmation for x minutes/seconds, if found unconfirmed thereafter, proceed to expire token process
    pub fn wait_to_expire_forgot_password_token(delay: u64, email: String ) {
        debug!("wait_to_expire_forgot_password_token({:?}, {:?}).......................<<", &delay, &email);
        thread::spawn(move || {
            let duration = time::Duration::from_millis(delay+3000); // additional 3 sec or 3*1000 milliseconds
            debug!("Time before sleep: {:?}", time::Instant::now());
            thread::sleep(duration);
            debug!("Time after sleep: {:?}", time::Instant::now());
            let result = Self::forgot_password_expired(&email);
            match result {
                Ok(msg) => debug!("Successfully expired forgot password token: {:?}", msg),
                Err(msg) => error!("Failed to expire forgot password token: {:?}", msg),
            }
        });
    }    
}

#[cfg(test)]
mod tests {
   use super::*;
   use crate::registration::Registration;
   use crate::messages::Msg;

   #[test]
    fn utest_email_not_exist() {
        let login = Login {
            email: "invalid email $ unit - test".to_string(),
            password: "".to_string(),
            status: "".to_string(),
        };
        let result = login.process().unwrap_or_else(|error| error);
        assert_eq!(result, Msg::InvalidCredentials);
    }
 
    #[test]
    fn utest_invalid_credentials() {
        let email = "invalid_credentials@unit.tst";
        Registration::new(&email, "Pass123~", "Pass123~").unwrap_or_else(|_| Msg::NewRegistrationFailed);
        let login = Login {
            email: email.to_string(),
            password: "".to_string(),
            status: "".to_string(),
        };
        let result = login.process().unwrap_or_else(|error| error);
        assert_eq!(result, Msg::InvalidCredentials);
        Registration::cancel(&email).unwrap_or_else(|error| error); // delete this registration, so as to enable next test run
    }
    
}