login_app 0.1.5

A generic login module for web apps
Documentation
// utils.rs
// utility functions implemented here;
use rand::{thread_rng, Rng};
use bcrypt::{DEFAULT_COST, hash as bhash};
use ::log::*; // external crate for macros such as debug!, info!, so on
use std::collections::HashMap;
use std::process::Command;
use std::thread;
use regex::Regex;

use crate::messages::Msg;

pub fn generate_token() -> String {
    let token_length: usize = super::app_config("token_length").parse().unwrap();
    let characters = super::app_config("token_chars");
    let chars_len: usize = characters.as_bytes().len();
    let charset: &[u8] = characters.as_bytes();
    let mut rng = thread_rng();
    let result: String = (0..token_length).map(|_| {
        let index = rng.gen_range(0, chars_len);
        charset[index] as char
    }).collect();
    result
}   

pub fn generate_id() -> String { // for session id in web pages
    let len: usize = super::app_config("id_length").parse().unwrap();
    let chars = super::app_config("id_chars");
    let chars_len: usize = chars.as_bytes().len();
    let charset: &[u8] = chars.as_bytes();
    let mut rng = thread_rng();
    let result: String = (0..len).map(|_| {
        let index = rng.gen_range(0, chars_len);
        charset[index] as char
    }).collect();
    result
}    
pub fn hash(an_item: &str) -> String {
    bhash(an_item, DEFAULT_COST).unwrap()
}

// For example: String such as "email=test1&password=test2&repeat-password=test3" will be split
// into a hashmap; here pair separator is &, value separator is =
pub fn str_to_map(data: &str, pair_separator: char, value_separator: char) -> HashMap<String, String> {
    let pairs: Vec<&str> = data.split(|c| c == pair_separator).collect();
    debug!("Pairs are: {:?}", &pairs);
    let mut map = HashMap::new();
    for pair in pairs {
        let result: Vec<&str> = pair.split(|c| c == value_separator).collect();
        map.insert(result[0].to_string(), result[1].to_string());
    }
    debug!("Hashmap is: {:?}", &map);
    map
}


#[derive(Debug)]
pub struct Email {
    from: String,
    to: String,
    subject: String,
    body: String,
}

impl Email {
    // constructor
    pub fn new(from: &str, to: &str, subject: &str, body: &str) -> Email {
        Email { 
            from: from.to_string(), 
            to: to.to_string(), 
            subject: subject.to_string(),
            body: body.to_string()
        }
    }

    pub fn send(&self) { // email is sent, using msmtp, an SMTP Client, installed in linux machine
        let from = self.from.clone();
        let to = self.to.clone();
        let subject = self.subject.clone();
        let body = self.body.clone();
        thread::spawn(move || {
            // email message is echoed into msmtp app
            let mail_cmd = format!("echo -e 'From: {}\nSubject: {}\n\n{}' | msmtp {}", from, subject, body, to);
            let output = Command::new("sh")
                            .arg("-c")
                            .arg(mail_cmd)
                            .output()
                            .expect("failed to execute sh command echo hello!!!");
            debug!("msmtp output: {:?}", output);
        });        
    }
}

#[derive(Debug)]
pub struct Validator {}

impl Validator {
    // constructor
    pub fn validate_password(key: String) -> Vec<Msg> {
        let password = Password { key };
        password.validate_pattern()
    }
    pub fn validate_email_id_pattern(id: String) -> bool {
        let email_id = EmailID { id };
        email_id.validate_pattern()
    }
}

#[derive(Debug)]
struct EmailID {
    id: String
}

impl EmailID {
    fn validate_pattern(&self) -> bool {
        let re = Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$");
        match re {
            Ok(re) => re.is_match(&self.id), 
            Err(error) => {
                error!("{:?} occurred in utils::EmailID::validate_pattern()", error);
                false
            }
        }
    }
}

#[derive(Debug)]
struct Password {
    key: String,
}

impl Password {

    fn validate_pattern(&self) -> Vec<Msg> { // covers password related validations
        
        let mut errors = vec![];

        if !self.has_lowercase() {
            errors.push(Msg::PasswordHasNoLowercase)  
        }
        if !self.has_uppercase() {
            errors.push(Msg::PasswordHasNoUppercase)
        }
        if !self.has_number() {
            errors.push(Msg::PasswordHasNoNumber)
        }
        if !self.has_spl_chars() {
            errors.push(Msg::PasswordHasNoSplChar)
        }
        if !self.length_ok() {
            errors.push(Msg::PasswordLengthNotOk)
        }

        errors
    }

    fn has_lowercase(&self) -> bool { // checks whether password contains at least one lowercase or not
        match Regex::new(r"^.*[a-z]+.*$") {
            Ok(re) => re.is_match(&self.key), 
            Err(error) => {
                error!("{:?} occurred in utils::Password::has_lowercase()", error);
                false
            }
        }
    }

    fn has_uppercase(&self) -> bool { // checks whether password contains at least one uppercase or not
        match Regex::new(r"^.*[A-Z]+.*$") {
            Ok(re) => re.is_match(&self.key), 
            Err(error) => {
                error!("{:?} occurred in utils::Password::has_uppercase()", error);
                false
            }
        }
    }

    fn has_number(&self) -> bool { // checks whether password contains at least one number or not
        match Regex::new(r"^.*[0-9]+.*$") {
            Ok(re) => re.is_match(&self.key), 
            Err(error) => {
                error!("{:?} occurred in utils::Password::has_number()", error);
                false
            }
        }
    }

    fn has_spl_chars(&self) -> bool { // checks whether password contains at least one special character or not
        let spl_chars = super::app_config("permitted_special_characters_in_password"); // retrieve special characters from file named settings.toml
        let re_str = r"^.*[".to_owned() + &spl_chars + "]+.*$";
        match Regex::new(&re_str) {
            Ok(re) => re.is_match(&self.key),
            Err(error) => {
                error!("{:?} occurred in utils::Password::has_spl_chars()", error);
                false
            }
        }
    }

    fn length_ok(&self) -> bool {
        let passwd_len = super::app_config("min_password_length");
        let re_str = r"^[\s\S]{".to_owned() + &passwd_len + ",}$";
        match Regex::new(&re_str) {
            Ok(re) => re.is_match(&self.key),
            Err(error) => {
                error!("{:?} occurred in utils::Password::length_ok", error);
                false
            }
        }
    }
}  // end of impl Password


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

    #[test]
    fn utest_valid_email_pattern() {
        let email_id = EmailID{ id: "sample1_email@integration.test".to_string() };
        assert_eq!(email_id.validate_pattern(), true)
    }

    #[test]
    fn utest_invalid_email_pattern() {
        let email_id = EmailID{ id: "aaaa".to_string() };
        assert_eq!(email_id.validate_pattern(), false)
    }

    #[test]
    fn utest_password_contains_no_lowercase() {
        let result = Password{ key: "PASS123".to_string() }.has_lowercase();
        assert_eq!(result, false)
    }

    #[test]
    fn utest_password_contains_lowercase() {
        let result = Password{ key: "pASS123".to_string() }.has_lowercase();
        assert_eq!(result, true)
    }

    #[test]
    fn utest_password_contains_no_uppercase() {
        let result = Password{ key: "pass123".to_string() }.has_uppercase();
        assert_eq!(result, false)
    }

    #[test]
    fn utest_password_contains_uppercase() {
        let result = Password{ key: "PASS123".to_string() }.has_uppercase();
        assert_eq!(result, true)
    }

    #[test]
    fn utest_password_contains_no_number() {
        let result = Password{ key: "pass".to_string() }.has_number();
        assert_eq!(result, false)        
    }

    #[test]
    fn utest_password_contains_number() {
        let result = Password{ key: "pass2".to_string() }.has_number();
        assert_eq!(result, true)        
    }

    #[test]
    fn utest_password_length_ok() {
        let result = Password{ key: "pass1234".to_string() }.length_ok();
        assert_eq!(result, true)
    }

    #[test]
    fn utest_password_length_not_ok() {
        let result = Password{ key: "pass34".to_string() }.length_ok();
        assert_eq!(result, false)        
    }

    #[test]
    fn utest_password_contains_no_special_character() {
        let result = Password{ key: "PASS123".to_string() }.has_spl_chars();
        assert_eq!(result, false)
    }

    #[test]
    fn utest_password_contains_special_character() {
        let result = Password{ key: "pASS12*".to_string() }.has_spl_chars();
        assert_eq!(result, true)
    }

}
 // End of mod tests