login_app 0.1.5

A generic login module for web apps
Documentation
// registration.rs
// registration related struct/functions are implemented here
use chrono::prelude::*;

use ::log::*; // external crate for macros such as debug!, info!, so on
use std::collections::HashMap;
use std::ops::Sub;
use std::{thread, time};
//use serde_json::map::Map;
//use serde_json::value::Value;

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


#[derive(Debug)]
pub struct Registration {
    email: String,
    password: String,
    repeat_password: String,
    token: String,
}

impl Registration {
    // constructor
    pub fn instance(email: &str, password: &str, repeat_password: &str, token: &str) -> Registration {
        Registration {
            email: email.to_string(), 
            password: password.to_string(), 
            repeat_password: repeat_password.to_string(), 
            token: token.to_string()
        }
    }
    pub fn new(email: &str, pwd: &str, repeat_pwd: &str) -> Result<Msg, Vec<Msg>> {
        let new_regn = Self::instance(email, pwd, repeat_pwd, &utils::generate_token());
        match new_regn.validate() {
            Ok(_) => {
                new_regn.add()
            },
            Err(messages) => {
                error!("{:?} occurred in Registration::new(..)", messages);
                Err(messages)
            }
        }
    }

    pub fn from_map(map: &HashMap<String, String>) -> Result<Msg, Vec<Msg>> {
        Self::new(
            map.get("email").unwrap(),
            map.get("password").unwrap(),
            map.get("repeat-password").unwrap()
        )
    }
    pub fn from_json_map(map: &serde_json::map::Map<String, serde_json::value::Value>) -> Result<Msg, Vec<Msg>> {
        Self::new(
            map.get("email").unwrap().as_str().unwrap(),
            map.get("password").unwrap().as_str().unwrap(),
            map.get("repeat-password").unwrap().as_str().unwrap(),            
        )
    }
    pub fn from_email_id(id: &str) -> Registration { // Registration struct using email alone
        Self::instance(id, "", "", "")
    }
    
    // getters and setters
    pub fn email(&self) -> String {
        self.email.clone()
    }
    fn password(&self) -> String {
        self.password.clone()
    }
    pub fn password_hashed(&self) -> String {
        utils::hash(&self.password())        
    }
    pub fn token(&self) -> String {
        self.token.clone()
    }
    // operations
    pub fn initial_status(&self) -> String {
        Msg::ConfirmationPending.description()
    }
    pub fn initial_remarks(&self) -> String {
        Msg::RegistrationInitialRemarks.description()
    }

    fn add(&self) -> Result<Msg, Vec<Msg>> {
        match db::add_registration(&self) {
            Ok(row_count) => {
                self.send_registration_confirmation_email();
                debug!("New Registration success: {:?} row(s) inserted", row_count);
                Ok(Msg::EmailSentToCompleteRegistration)
            }
            Err(error) => {
                error!("{:?} occurred in Registration::add(..)", error);
                Err(vec![Msg::EmailExist])
            }
        }
    }

    fn send_registration_confirmation_email(&self) {
        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!("Registration Confirmation Token is: {}.\n\nPlease copy this token into confirmation window to complete registration", self.token());
        let email_to = self.email();
        let subject = "Login-app Registration Confirmation Email";
        let email = utils::Email::new(&email_from, &email_to, &subject, &message_body);
        email.send();
    }
    // await user confirmation for x minutes/seconds, if found unconfirmed thereafter, proceed to remove registration
    pub fn wait_to_remove_unconfirmed(delay: u64, email: String ) {
        debug!("wait_to_removed_unconfirmed({:?}, {:?}).......................<<", &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());
            match db::get_unconfirmed_user_record_for(&email) {
                Ok(user_record) => {
                    debug!("User Record: {:?}", user_record);
                    Self::delete_expired_registration(&email)
                },
                Err(message) => {
                    error!("{:?} occurred in Registration::wait_to_remove_unconfirmed()!", message);
                    Err(Msg::GetUnconfirmedUserRecordFailed)
                }
            }
        });
    }

    pub fn cancel(email: &str) -> Result<Msg, Msg> { // cancel mark record status to 'cancelled'
        debug!("cancel({:?})........................<<", &email);
        if db::email_status_record_count(email, "active") < 1 {
            return Err(Msg::NoEmailFoundCancellationFailed);
        }
        match db::cancel_registration(email) {
            Ok(row_count) => {
                debug!("{:?} row(s) deleted; Cancellation success", row_count);
                Ok(Msg::CancellationSuccess)
            },
            Err(message) => {
                error!("{:?} occurred in Registration::cancel(..)", message);
                Err(Msg::CancellationFailed)
            }
        }
    } 

    pub fn delete(email: &str) -> Result<Msg, Msg> { // delete record from database
        debug!("delete({:?})........................<<", &email);
        if db::email_status_record_count(email, "active") < 1 {
            return Err(Msg::NoEmailFoundDeleteFailed);
        }
        match db::delete_registration(email) {
            Ok(row_count) => {
                debug!("{:?} row(s) deleted; Cancellation success", row_count);
                Ok(Msg::DeletionSuccess)
            },
            Err(message) => {
                error!("{:?} occurred in Registration::delete(..)", message);
                Err(Msg::DeletionFailed)
            }
        }
    } 

    pub fn delete_expired_registration(email: &str) -> Result<Msg, Msg> {
        debug!("delete_expired_registration({:?}).........................<<", &email);
        if !Self::is_registration_expired(email) {
            return Err(Msg::ExpiryCheckFailed);
        }
        match db::delete_registration(email) {
            Ok(row_count) => {
                debug!("{:?} row(s) deleted; delete expired registration success", row_count);
                Ok(Msg::DeleteExpiredRegistrationSuccess)
            },
            Err(message) => {
                error!("{:?} occurred in Registration::delete_expired_registration()", message);
                Err(Msg::DeleteExpiredRegistrationFailed)
            }
        }
    }

    fn is_registration_expired(email: &str) -> bool {
        debug!("is_registration_expired({:?})............................<<", &email);
        match db::get_user_record_for(email) {
            Ok(user_record) => {
                debug!("User Record: {:?}", user_record);
                let created_on = DateTime::parse_from_str(&user_record.created_on(), "%Y-%m-%d %H:%M:%S%.9f %z").unwrap();
                Self::is_waiting_time_elapsed_since(created_on)
            },
            Err(message) => {
                error!("{:?} occurred in is_registration_expired", message);
                false
            }
        }
    }
    
    fn is_waiting_time_elapsed_since(created_on: DateTime<FixedOffset>) -> bool {
        debug!("is_waiting_time_elapsed_since({:?})...................<<", &created_on);
        // check difference between created_on and time_now, 
        // if the difference exceeds expiry limit, cancel such record
        let time_now_str = Local::now().to_string();
        let time_now = DateTime::parse_from_str(&time_now_str, "%Y-%m-%d %H:%M:%S%.9f %z").unwrap();
        debug!("Time now: {:?}", &time_now);
        let elapsed_time = time_now.sub(created_on);

        let waiting_time: i64 = super::app_config("token_validity_time").parse().unwrap();
        let waiting_unit = super::app_config("token_validity_unit");
        debug!("Elapsed time: {:?}", &elapsed_time);
        match waiting_unit.as_ref() {
            "minutes" => elapsed_time.num_minutes() >= waiting_time,
            "seconds" => elapsed_time.num_seconds() >= waiting_time,
            _ => false,
        }
    }

    pub fn confirm(token: &str) -> Result<Msg, Msg> {
        debug!("confirm( {:?} ).............................<<", token); 
        if db::token_count(token) < 1 {
            return Err(Msg::InvalidToken);
        }
        let mut user = db::get_user_for_token(token)?;
        if user.status() != Msg::ConfirmationPending.description() {
            error!("Registration is already confirmed");
            return Err(Msg::RegistrationAlreadyConfirmed);
        }
        let clear_token = "";
        user.set_status(&Msg::Active.description());
        let row_count = user.update_with_token(&clear_token);
        if row_count == 0 {
            error!("{:?} row(s) affected in registration::confirm(..) while updating status", row_count);
            Err(Msg::RegistrationConfirmationFailed)
        } else {
            debug!("{:?} row(s) updated with confirmed status", row_count);
            Ok(Msg::RegistrationConfirmed)
        }
   }

   fn validate(&self) -> Result<Msg, Vec<Msg>> {
        debug!("validate().............................<<");
        let mut errors = vec![];

        if db::email_status_record_count(&self.email, "active") != 0 {
            error!("email NOT available");
            errors.push(Msg::EmailExist);
        }
        if self.password != self.repeat_password {
            error!("passwords DO NOT match");
            errors.push(Msg::PasswordsDoNotMatch);
        }
        if !utils::Validator::validate_email_id_pattern(self.email.clone()) {
            error!("INVALID email pattern");
            errors.push(Msg::InvalidEmailPattern);
        }
        let mut messages = utils::Validator::validate_password(self.password.clone());
        errors.append(&mut messages);

        if errors.is_empty() {
            Ok(Msg::ValidationsPassed)
        } else {
            Err(errors)
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    fn sample1() -> Registration {
        Registration::instance("unit test", "pass1", "pass2", "")
    }

    fn sample2() -> Registration {
        Registration::instance("unit test", "pass1", "pass1", "")
    }

    #[test]
    fn utest_passwords_do_not_match() {
        let mut result = vec![];
        if let Err(errors) = sample1().validate() { result = errors; }
        assert!( result.contains(&Msg::PasswordsDoNotMatch) )
    }

    #[test]
    fn utest_passwords_match() {
        let mut result = vec![];
        if let Err(errors) = sample2().validate() { result = errors; }
        assert!( !result.contains(&Msg::PasswordsDoNotMatch) )
    }

    #[test]
    fn utest_email_available() {
        let regn = Registration::from_email_id("unit test");
        let mut result = vec![];
        if let Err(errors) = regn.validate() { result = errors; }        
        assert!( !result.contains(&Msg::EmailExist) )
    }
    
    #[test]
    fn utest_invalid_token() {
        let result = Registration::confirm("an invalid token").unwrap_or_else(|error| error);
        assert_eq!(result, Msg::InvalidToken);
    }

}
 // End of mod tests