use crate::schema::{DatabaseError, DbConn}; use argon2::password_hash::SaltString; use argon2::PasswordHash; use argon2::PasswordVerifier; use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher}, Argon2, }; use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel_derive_enum::DbEnum; use serde::{Deserialize, Serialize}; use uuid::Uuid; use validator::{Validate, ValidationError}; #[derive(Debug, Serialize, Deserialize, DbEnum, Clone, Copy, PartialEq)] pub enum Role { Admin, User, } table! { users(id) { id -> diesel::sql_types::Uuid, username -> VarChar, email -> VarChar, role -> crate::schema::users::RoleMapping, } } table! { pwd(user_id) { user_id -> diesel::sql_types::Uuid, password -> VarChar, } } #[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] #[table_name = "pwd"] struct Pwd { user_id: Uuid, password: String, } #[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] #[table_name = "users"] pub struct User { pub id: Uuid, pub username: String, pub email: String, pub role: Role, } #[derive(Debug, Serialize, Deserialize)] pub struct UserWithToken { #[serde(flatten)] pub user: User, pub jwt: String, } #[derive(Serialize, Deserialize, Debug)] pub struct Login { pub username: String, pub password: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LoginResult { pub result: bool, pub user: Option, } #[derive(Serialize, Deserialize, Debug, Validate, Clone)] pub struct Register { #[validate( length(min = 1), custom(function = "unique_username", arg = "&'v_a diesel::PgConnection") )] pub username: String, #[validate( email, custom(function = "unique_email", arg = "&'v_a diesel::PgConnection") )] pub email: String, #[validate(length(min = 10), must_match = "password_repeat")] pub password: String, pub password_repeat: String, } pub async fn insert_user(conn: DbConn, new_user: Register) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let password_hash = argon2 .hash_password(new_user.password.as_bytes(), &salt)? .to_string(); Ok(conn .run(move |c| { c.transaction(|| { let id = Uuid::new_v4(); diesel::insert_into(users::table) .values(User { id: id.clone(), username: new_user.username, email: new_user.email, role: Role::User, }) .execute(c)?; diesel::insert_into(pwd::table) .values(Pwd { user_id: id, password: password_hash, }) .execute(c) }) }) .await?) } pub async fn login(conn: DbConn, login: Login) -> Result { conn.run(move |c| -> Result { let id: Uuid = users::table .filter(users::username.eq(&login.username)) .or_filter(users::email.eq(&login.username)) .select(users::id) .first(c)?; let pwd: String = pwd::table .filter(pwd::user_id.eq(id)) .select(pwd::password) .first(c)?; let parsed_hash = PasswordHash::new(&pwd)?; if Argon2::default() .verify_password(&login.password.as_bytes(), &parsed_hash) .is_ok() { let user: User = users::table.find(id).first(c)?; Ok(LoginResult { result: true, user: Some(user), }) } else { Ok(LoginResult { result: false, user: None, }) } }) .await } pub async fn get_user(conn: DbConn, id: Uuid) -> Result { Ok(conn .run(move |c| users::table.filter(users::id.eq(id)).first(c)) .await?) } pub fn unique_username( username: &String, conn: &diesel::PgConnection, ) -> Result<(), ValidationError> { match users::table .count() .filter(users::username.eq(username)) .get_result(conn) { Ok(0) => Ok(()), Ok(_) => Err(ValidationError::new("User already exists")), Err(_) => Err(ValidationError::new("Database error while validating user")), } } pub fn unique_email(email: &String, conn: &diesel::PgConnection) -> Result<(), ValidationError> { match users::table .count() .filter(users::email.eq(email)) .get_result(conn) { Ok(0) => Ok(()), Ok(_) => Err(ValidationError::new("email already exists")), Err(_) => Err(ValidationError::new( "Database error while validating email", )), } }