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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
//! A set of base functions that are used to do all the underlying work of auth. This includes user and session management.
use argon2::password_hash::Salt;
use argon2::Argon2;
use argon2::PasswordHasher;
use argon2::PasswordVerifier;
use dotenvy;
use rand::distributions::DistString;
use rand::Rng;
use sqlx::Postgres;
use sqlx::QueryBuilder;
use serde;
use sqlx::FromRow;
use serde::ser::SerializeStruct;

/// Basic information about a user. Note that `realm` can be a arbitrary string and you can use to figure out a group a user belongs to like `"admin"`.
#[derive(serde::Deserialize, Clone)]
pub struct Credentials {
    pub user_name: String,
    pub password: String,
    pub realm: String, // the realm can be used to specify the set of permissions of a user
}

/// Information about a session. Each session is associated with a `user_name`.
#[derive(Debug, FromRow)]
pub struct Session {
    pub user_name: String,
    pub session_token: String,
    pub time_to_die: chrono::DateTime<chrono::Utc>
}

/// Error return type of `add_user`
#[derive(Debug)]
pub enum AddUserReturn {
    Good(),
    UserNotUnique(),
    SaltFailed(),
    HashError(String),
    InsertError(String),
}

/// Error return type of `validate_user`
#[derive(Debug)]
pub enum UserValidated {
    Validated(),
    NotValidated(),
}

/// Error return type of `generate_session`
#[derive(Debug)]
pub enum SessionGeneratedErr {
    UserNotValid(),
    FailedToAddToDatabase(String),
}

/// Return type of `validate_session`
#[derive(Debug)]
pub enum SessionValidated {
    ValidSession(),
    InvalidSession(), 
}

/// Return type of `invalidate_session`
pub enum SessionInvalided {
    SucessfullyInvalidated(),
    DidNotExist(),
    Error(String),
}

#[derive(FromRow)]
struct UserRow {
    user_name: String,
    phc: String,
    realms: String,
}

impl serde::Serialize for Session{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut s = serializer.serialize_struct("Session", 3)?;
        s.serialize_field("user_name", &self.user_name)?;
        s.serialize_field("session_token", &self.session_token)?;
        s.serialize_field("time_to_die", &self.time_to_die.to_rfc3339())?;
        s.end()
    }
}

/// Generates a random string between the given ranges.
fn gen_rand_string_between(min_len: u16, max_len: u16) -> String {
    let mut rng_source = rand::thread_rng();
    let str_len: usize = rng_source.gen_range((min_len + 1)..max_len).into();

    rand::distributions::Alphanumeric.sample_string(&mut rng_source, str_len)
}

/// Generates a random string of a given length.
fn gen_rand_string_of_len(len: u16) -> String {
    let mut rng_source = rand::thread_rng();
    rand::distributions::Alphanumeric.sample_string(&mut rng_source, len.into())
}

/// Provides a connection to a postgres server. This requires `DATABASE_URL` to be set.
pub async fn connect_to_db_get_pool() -> Result<sqlx::Pool<Postgres>, sqlx::Error> {
    let dotenv_database_url_result = dotenvy::var("DATABASE_URL");
    let data_baseurl = match dotenv_database_url_result {
        Ok(data_baseurl) => data_baseurl,
        Err(err) => err.to_string(),
    };

    // Connect to database
    let pool = match sqlx::postgres::PgPoolOptions::new()
        .max_connections(5)
        .connect(&data_baseurl)
        .await
    {
        Ok(pool) => pool,
        Err(err) => return Err(err),
    };

    return Ok(pool);
}

/// Builds the projects current default Argon2 harsher. This exists so that
/// we avoid `default` and grantee the same settings for the harsher across the
/// As some point in the future it would be good if this was loaded
/// from ENV.
fn build_argon2_hasher<'a>() -> Argon2<'a> {
    argon2::Argon2::new(
        argon2::Algorithm::Argon2id,
        argon2::Version::V0x13,
        argon2::Params::new(1500, 2, 1, None).expect("ADD A ERROR"),
    )
}

/// Will create a session for the provided user. The users information is stored
/// in `credentials`. Once the session is generated it will be stored in the
/// DB. The validity and state of the session can be changed later with
/// `validate_session` and `invalidate_session`.
pub async fn generate_session(
    credentials: &Credentials,
    pool: &sqlx::Pool<Postgres>,
    secs_after_creation_to_die: i64,
) -> Result<Session, SessionGeneratedErr> {
    // Check if user is valid in DB we will not use this info later so we toss it
    let _valid_user = match validate_user(&credentials, pool).await {
        UserValidated::Validated() => (),
        UserValidated::NotValidated() => return Err(SessionGeneratedErr::UserNotValid()), 
    };
    
    let session = Session{
        user_name: credentials.user_name.to_string(),
        session_token: gen_rand_string_of_len(100),
        time_to_die: chrono::Utc::now() + chrono::TimeDelta::minutes(1)
    };

    let mut sql_insert_session_builder = sqlx::QueryBuilder::new("INSERT INTO sessions (user_name, session_token, time_to_die) VALUES (");
    sql_insert_session_builder.push_bind(&session.user_name);
    sql_insert_session_builder.push(",");
    sql_insert_session_builder.push_bind(&session.session_token);
    sql_insert_session_builder.push(",");
    sql_insert_session_builder.push_bind(session.time_to_die.to_rfc3339());
    sql_insert_session_builder.push("::timestamp);");
    
    let sql_insert_session = sql_insert_session_builder.build().execute(pool).await;

    match sql_insert_session {
        Ok(query_result) => match  query_result.rows_affected() {
            1 => Ok(session),
            _ => {
                println!("Modified: {}", query_result.rows_affected());
                Err(SessionGeneratedErr::FailedToAddToDatabase("Got more than 0 changes".to_string()))
            }
        },
        Err(err) => Err(SessionGeneratedErr::FailedToAddToDatabase(format!("{}", err.to_string())))
    }
}

/// `validate_session`
///
/// Given a session will check if its valid in the database.
pub async fn validate_session(
    session: &Session,
    pool: &sqlx::Pool<Postgres>,
) -> SessionValidated {
    let sql_session = "SELECT user_name, session_token FROM sessions WHERE user_name=$1 AND session_token=$2 AND time_to_die > now() at time zone ('utc');";

    let (db_user_name, db_session_token):(String, String) = match sqlx::query_as(&sql_session)
    .bind(&session.user_name)
    .bind(&session.session_token)
    .fetch_optional(pool).await {
        Ok(option) => match  option {
            Some(res) => res,
            None => return SessionValidated::InvalidSession()
        },
        Err(_err) => return SessionValidated::InvalidSession()
    };

    // This is double checking the work of the db so may not really be needed
    // However better safe than sorry
    if db_user_name == session.user_name && db_session_token == session.session_token {
        return SessionValidated::ValidSession();
    } else {
        return SessionValidated::InvalidSession();
    }
}

/// `invalidate_session`
/// 
/// If the session exists in the database will drop the session. This means the user will no longer be able use this session from any request. 
pub async fn invalidate_session(
    session: &Session,
    pool: &sqlx::Pool<Postgres>,
) -> SessionInvalided {
    let sql_session = "DELETE FROM sessions WHERE user_name=$1 AND session_token=$2;";

    match sqlx::query(&sql_session)
    .bind(&session.user_name)
    .bind(&session.session_token)
    .execute(pool).await {
        Ok(res) => match res.rows_affected() {
           1 => SessionInvalided::SucessfullyInvalidated(),
           _ => SessionInvalided::DidNotExist(),
        },
        Err(err) => return SessionInvalided::Error(err.to_string())
    }
}


/// `add_user`
/// 
/// Given the user information. Will add the user to the database. 
/// This will allow the user to generate sessions with the given user information.
pub async fn add_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> AddUserReturn {
    // Generate Hasher instancef
    // Generate salt
    let rand_str = gen_rand_string_of_len(Salt::RECOMMENDED_LENGTH.try_into().unwrap());
    let hasher = build_argon2_hasher();

    // Encode string to phc
    let hash = hasher.hash_password(credentials.password.as_bytes(), &rand_str);
    let phc_string = match hash {
        Ok(phc_string) => phc_string.to_string(),
        Err(err) => {
            return AddUserReturn::HashError(format!(
                "with pass {}, salt {}, got {}",
                credentials.password,
                4,
                err.to_string()
            ))
        }
    };
    println!("with pass {}, salt {}", credentials.password, rand_str);

    // Build SQL
    let mut sql_insert_user_builder: QueryBuilder<Postgres> =
        sqlx::QueryBuilder::new("INSERT INTO users(user_name, phc, realms) VALUES (");
    sql_insert_user_builder
        .push_bind(&credentials.user_name)
        .push(",")
        .push_bind(phc_string)
        .push(",")
        .push_bind(&credentials.realm)
        .push(") LIMIT 1;");

    let sql_insert_build: sqlx::query::QueryAs<Postgres, UserRow, sqlx::postgres::PgArguments> =
        sql_insert_user_builder.build_query_as::<UserRow>();
    match sql_insert_build.fetch_optional(pool).await {
        Ok(_) => return AddUserReturn::Good(),
        Err(err) => return AddUserReturn::InsertError(err.to_string()),
    }
}

/// `validate_user`
/// 
/// Given the user credentials provided will check if the credentials are valid and in the database.
pub async fn validate_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> UserValidated {
    //! 1. Get user from data base
    //! Build SQL for SELECT
    let mut sql_user_builder: QueryBuilder<Postgres> =
        sqlx::QueryBuilder::new("SELECT user_name, phc, realms FROM users WHERE user_name=");

    sql_user_builder
        .push_bind(credentials.user_name.clone())
        .push(";");

    println!("Got pass:{}", &credentials.password);
    println!("Got name:{}", &credentials.user_name);
    println!("SQL:{}", sql_user_builder.sql().to_string());

    let user_info_option: Option<UserRow> = match sql_user_builder
        .build_query_as::<UserRow>()
        .fetch_optional(pool)
        .await
    {
        Ok(user_info_option) => user_info_option,
        Err(_) => {
            println!("Fail at fetch");
            return UserValidated::NotValidated();
        }
    };

    let user_info: UserRow = match user_info_option {
        Some(user_info) => user_info,
        None => {
            println!("Fail at User info up");
            return UserValidated::NotValidated();
        }
    };

    // once we have the user info and have confirmed it exists we can check the
    let existing_password_hash: argon2::PasswordHash =
        match argon2::PasswordHash::new(&user_info.phc) {
            Ok(hasher) => hasher,
            Err(_) => {
                println!("Fail at hash");
                return UserValidated::NotValidated();
            }
        };

    let argon_hasher = build_argon2_hasher(); // Builds Argon hasher is current default settings
    match argon_hasher.verify_password(credentials.password.as_bytes(), &existing_password_hash) {
        Ok(_) => UserValidated::Validated(),
        Err(_) => UserValidated::NotValidated(),
    }
}

#[cfg(test)]
mod auth_tests {
    use super::*;
    async fn connect_to_pool() -> sqlx::Pool<Postgres>{
        match  connect_to_db_get_pool().await {
            Ok(pool) => pool,
            Err(_) => panic!("could not connect to db"),
        }
    }

    async fn complete_migrations(pool: &sqlx::Pool<Postgres>) -> () {
        let sqlx_migrator = sqlx::migrate!();
        let _migration_undo = match sqlx_migrator.undo(pool, 0).await {
            Ok(_) => true,
            Err(err) => return assert!(false, "migrator failed with err: {}", err.to_string()),
        };

        let _migration_run = match sqlx_migrator.run(pool).await {
            Ok(_) => true,
            Err(_) => return assert!(false, "migrator failed run"),
        };
    }
    #[actix_web::test]
    async fn get_session() {
        let pool = connect_to_pool().await;
        complete_migrations(&pool).await;

        let creds = Credentials {
            user_name: "test_user".to_string().to_owned(),
            password: "my_pass".to_string().to_owned(),
            realm: "user".to_string().to_owned(),
        };

        let _add_user_result = match add_user(&creds, &pool).await {
            AddUserReturn::Good() => (),
            _ => (),
        };

        match generate_session(&creds, &pool, 100).await {
            Ok(session) => assert!(session.user_name == creds.user_name),
            Err(err) => assert!(false, "{:?}", err)
        }
    }

    #[actix_web::test]
    async fn verify_session() {
        let pool = connect_to_pool().await;
        complete_migrations(&pool).await;

        let creds = Credentials {
            user_name: "test_user".to_string().to_owned(),
            password: "my_pass".to_string().to_owned(),
            realm: "user".to_string().to_owned(),
        };

        let _add_user_result = match add_user(&creds, &pool).await {
            AddUserReturn::Good() => (),
            _ => (),
        };

        let session = match generate_session(&creds, &pool, 100).await {
            Ok(session) => session,
            Err(err) => return assert!(false, "{:?}", err)
        };

        match validate_session(&session, &pool).await {
            SessionValidated::ValidSession() => assert!(true, "Session validated"),
            SessionValidated::InvalidSession() => assert!(false, "Session wrongly invalidated")
        }
    }

#[actix_web::test]
    async fn verify_session_invalid_token_end() {
        let pool = connect_to_pool().await;
        complete_migrations(&pool).await;

        let creds = Credentials {
            user_name: "test_user".to_string().to_owned(),
            password: "my_pass".to_string().to_owned(),
            realm: "user".to_string().to_owned(),
        };

        let _add_user_result = match add_user(&creds, &pool).await {
            AddUserReturn::Good() => (),
            _ => (),
        };

        let mut session = match generate_session(&creds, &pool, 100).await {
            Ok(session) => session,
            Err(err) => return assert!(false, "{:?}", err)
        };

        // Alter session token such that it no longer matches what is in the db
        let replace_last_char_with = match session.session_token.pop() {
            Some(c) => {
                if c == 'a' {
                    'b'
                } else {
                    'a'
                }
            }
            None => 'a'
        };

        session.session_token.push(replace_last_char_with);
        match validate_session(&session, &pool).await {
            SessionValidated::ValidSession() => assert!(false, "Session validated wrongly"),
            SessionValidated::InvalidSession() => assert!(true, "Session correctly invalidated")
        }
    }

    #[actix_web::test]
    async fn verify_session_invalid_user_name() {
        let pool = connect_to_pool().await;
        complete_migrations(&pool).await;

        let creds = Credentials {
            user_name: "test_user".to_string().to_owned(),
            password: "my_pass".to_string().to_owned(),
            realm: "user".to_string().to_owned(),
        };

        let _add_user_result = match add_user(&creds, &pool).await {
            AddUserReturn::Good() => (),
            _ => (),
        };

        let mut session = match generate_session(&creds, &pool, 100).await {
            Ok(session) => session,
            Err(err) => return assert!(false, "{:?}", err)
        };

        // Alter session token such that it no longer matches what is in the db
        session.user_name = "".to_string();
        match validate_session(&session, &pool).await {
            SessionValidated::ValidSession() => assert!(false, "Session validated wrongly"),
            SessionValidated::InvalidSession() => assert!(true, "Session correctly invalidated")
        }
    }

    #[actix_web::test]
    async fn invalidate_session_test() {
        let pool = connect_to_pool().await;
        complete_migrations(&pool).await;

        let creds = Credentials {
            user_name: "test_user".to_string().to_owned(),
            password: "my_pass".to_string().to_owned(),
            realm: "user".to_string().to_owned(),
        };

        let _add_user_result = match add_user(&creds, &pool).await {
            AddUserReturn::Good() => (),
            _ => (),
        };

        let mut session = match generate_session(&creds, &pool, 100).await {
            Ok(session) => session,
            Err(err) => return assert!(false, "{:?}", err)
        };

        match invalidate_session(&session, &pool).await {
            SessionInvalided::SucessfullyInvalidated() => assert!(true, "Session invalidated correctly"),
            _ => assert!(false, "Session invalidated error")
        }
    }
}