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
use crate::domain::entity::{Credentials, CredentialsId};
use crate::{
    application::interface::CredentialsRepository,
    infrastructure::services::password_hash::{hash_raw_password, verify_password},
};
use crate::{config::DatabaseConfig, infrastructure::gateway};
use anyhow::{Context, Result};

pub struct AuthClient<C: CredentialsRepository> {
    pub gateway: C,
}

#[cfg(feature = "mysql")]
impl AuthClient<gateway::mysql::MySqlGateway> {
    pub async fn new_mysql_client(
        db_configs: &DatabaseConfig,
    ) -> AuthClient<gateway::mysql::MySqlGateway> {
        let gateway = gateway::mysql::MySqlGateway::new(db_configs).await;

        Self { gateway }
    }
}

#[cfg(feature = "surreal")]
impl AuthClient<gateway::surreal::SurrealGateway> {
    pub async fn new_surreal_client(
        db_configs: &DatabaseConfig,
    ) -> AuthClient<gateway::surreal::SurrealGateway> {
        let gateway = gateway::surreal::SurrealGateway::new(db_configs).await;

        Self { gateway }
    }
}

impl<C: CredentialsRepository> AuthClient<C> {
    /// Register a new user and insert them into the database if user does not already exist
    pub async fn register(&self, user_name: &str, raw_password: &str) -> Result<CredentialsId> {
        match self.gateway.find_credentials_by_user_name(user_name).await {
            Ok(_) => {
                return Err(anyhow::anyhow!(
                    "Registration failed, credentials already exist"
                ))
            }
            Err(_) => {
                let hashed_password = hash_raw_password(raw_password);

                let credentials = Credentials::new(user_name, hashed_password.as_str());

                self.gateway
                    .insert_credentials(&credentials)
                    .await
                    .context("Registration failed, repository error")?;

                return Ok(credentials.credentials_id);
            }
        };
    }

    /// Matches credentials provided by the user with the what is in the database
    pub async fn verify_credentials(&self, user_name: &str, raw_password: &str) -> Result<()> {
        let creds = self
            .gateway
            .find_credentials_by_user_name(&user_name)
            .await?;

        verify_password(raw_password, &creds.hashed_password)
            .context("Username or Password did not match")
    }

    /// Deletes credentials from table
    pub async fn destroy_credentials(&self, user_name: &str) -> Result<()> {
        self.gateway
            .delete_credentials_by_user_name(user_name)
            .await
    }

    /// Update user name
    pub async fn update_user_name(
        &self,
        current_user_name: &str,
        new_user_name: &str,
    ) -> Result<()> {
        self.gateway
            .update_user_name(current_user_name, new_user_name)
            .await
    }

    /// Update user password
    pub async fn update_password(&self, user_name: &str, new_raw_password: &str) -> Result<()> {
        let new_hashed_password = hash_raw_password(new_raw_password);
        self.gateway
            .update_user_password(user_name, new_hashed_password.as_str())
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::helpers::mysql_configs;
    #[cfg(feature = "surreal")]
    use crate::helpers::surreal_configs;

    #[cfg(feature = "mysql")]
    #[tokio::test]
    async fn test_mysql_auth() {
        let db_configs = mysql_configs();
        let auth = AuthClient::new_mysql_client(&db_configs).await;

        // create random user creds
        let random_str = &uuid::Uuid::new_v4().to_string();
        let email = &random_str[..10];
        let password = "secret-test-password";
        let creds_id = auth.register(email, password).await.unwrap();
        assert_eq!(creds_id.len(), 36);

        // login attempt
        auth.verify_credentials(email, password).await.unwrap();
    }

    #[cfg(feature = "surreal")]
    #[tokio::test]
    async fn test_surreal_auth() {
        let db_configs = surreal_configs();
        let auth = AuthClient::new_surreal_client(&db_configs).await;

        // create random user creds
        let random_str = &uuid::Uuid::new_v4().to_string();
        let email = &random_str[..10];
        let password = "secret-test-password";
        let creds_id = auth.register(email, password).await.unwrap();
        assert_eq!(creds_id.len(), 36);

        // login attempt
        auth.verify_credentials(email, password).await.unwrap();
    }
}