1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// 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
    }
    
}