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;
pub const SESSION_VALID_FOR_SECONDS: i64 = 3600;
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct Credentials {
pub user_name: String,
pub password: String,
pub realm: String, }
#[derive(Debug, FromRow)]
pub struct Session {
pub user_name: String,
pub session_token: String,
pub time_to_die: chrono::DateTime<chrono::Utc>
}
#[derive(Debug)]
pub enum AddUserReturn {
Good(),
UserNotUnique(),
SaltFailed(),
HashError(String),
InsertError(String),
}
#[derive(Debug)]
pub enum DeleteUserReturn {
Good(), FailedToDeleteSessions(String),
BadUserOrPassword(),
DataBaseError(String),
}
#[derive(Debug)]
pub enum EndSessionReturn {
Ended(),
BadSession(),
DataBaseError(String),
}
#[derive(Debug)]
pub enum UserValidatedReturn {
Validated(),
NotValidated(),
}
#[derive(Debug)]
pub enum SessionGeneratedErr {
UserNotValid(),
FailedToAddToDatabase(String),
}
#[derive(Debug)]
pub enum SessionValidated {
ValidSession(),
InvalidSession(),
}
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()
}
}
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)
}
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())
}
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(),
};
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);
}
fn build_argon2_hasher<'a>() -> Argon2<'a> {
argon2::Argon2::new(
argon2::Algorithm::Argon2id,
argon2::Version::V0x13,
argon2::Params::new(1500, 2, 1, None).expect("Argon2 failed to generate harsher"),
)
}
pub async fn generate_session(
credentials: &Credentials,
pool: &sqlx::Pool<Postgres>,
secs_after_creation_to_die: i64,
) -> Result<Session, SessionGeneratedErr> {
let _valid_user = match validate_user(&credentials, pool).await {
UserValidatedReturn::Validated() => (),
UserValidatedReturn::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::seconds(secs_after_creation_to_die)
};
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())))
}
}
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()
};
if db_user_name == session.user_name && db_session_token == session.session_token {
return SessionValidated::ValidSession();
} else {
return SessionValidated::InvalidSession();
}
}
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())
}
}
pub async fn add_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> AddUserReturn {
let rand_str = gen_rand_string_of_len(Salt::RECOMMENDED_LENGTH.try_into().unwrap());
let hasher = build_argon2_hasher();
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);
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) => {
let _db_err = match err.as_database_error() {
Some(db_err) =>
match db_err.to_string().find("users_pkey") {
Some(_match) => return AddUserReturn::UserNotUnique(),
None => return AddUserReturn::InsertError(err.to_string())
}
None => return AddUserReturn::InsertError(err.to_string())
};
},
}
}
pub async fn delete_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> DeleteUserReturn {
let _valid_user = match validate_user(&credentials, pool).await {
UserValidatedReturn::Validated() => (),
UserValidatedReturn::NotValidated() => return DeleteUserReturn::BadUserOrPassword()
};
let mut sql_delete_session_builder = sqlx::QueryBuilder::new("DELETE FROM sessions WHERE user_name=");
sql_delete_session_builder.push_bind(&credentials.user_name).push(";");
let _sessions_deleted_option = match sql_delete_session_builder.build().fetch_optional(pool).await {
Ok(_) => (),
Err(err) => return DeleteUserReturn::FailedToDeleteSessions(err.to_string())
};
let mut sql_delete_use_builder = sqlx::QueryBuilder::new("DELETE FROM users WHERE user_name =");
sql_delete_use_builder.push_bind(&credentials.user_name).push(";");
let _user_deleted_option = match sql_delete_use_builder.build().fetch_optional(pool).await {
Ok(_) => return DeleteUserReturn::Good(),
Err(err) => return DeleteUserReturn::DataBaseError(err.to_string())
};
}
pub async fn end_sessions(session: &Session, pool: &sqlx::Pool<Postgres>) -> EndSessionReturn {
let _valid_session = match validate_session(session, pool).await {
SessionValidated::ValidSession() => (),
SessionValidated::InvalidSession() => return EndSessionReturn::BadSession()
};
let mut sql_delete_session_builder = sqlx::QueryBuilder::new("DELETE FROM sessions WHERE user_name=");
sql_delete_session_builder.push_bind(&session.user_name).push(";");
let _sessions_deleted_option = match sql_delete_session_builder.build().fetch_optional(pool).await {
Ok(_) => return EndSessionReturn::Ended(),
Err(err) => return EndSessionReturn::DataBaseError(err.to_string())
};
}
pub async fn validate_user(credentials: &Credentials, pool: &sqlx::Pool<Postgres>) -> UserValidatedReturn {
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(";");
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 UserValidatedReturn::NotValidated();
}
};
let user_info: UserRow = match user_info_option {
Some(user_info) => user_info,
None => {
println!("Fail at User info up");
return UserValidatedReturn::NotValidated();
}
};
let existing_password_hash: argon2::PasswordHash =
match argon2::PasswordHash::new(&user_info.phc) {
Ok(hasher) => hasher,
Err(_) => {
println!("Fail at hash");
return UserValidatedReturn::NotValidated();
}
};
let argon_hasher = build_argon2_hasher(); match argon_hasher.verify_password(credentials.password.as_bytes(), &existing_password_hash) {
Ok(_) => UserValidatedReturn::Validated(),
Err(_) => UserValidatedReturn::NotValidated(),
}
}