use uuid::Uuid; use crate::diesel::Connection; use crate::diesel::ExpressionMethods; use crate::diesel::QueryDsl; use argon2::password_hash::SaltString; use argon2::PasswordHash; use argon2::PasswordVerifier; use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher}, Argon2, }; use diesel::RunQueryDsl; use diesel_derive_enum::DbEnum; use rocket::{Build, Rocket}; use rocket_sync_db_pools::database; use serde::{Deserialize, Serialize}; use std::ops::Deref; use validator::{Validate, ValidationError}; #[database("gamenight_database")] pub struct DbConn(diesel::PgConnection); impl Deref for DbConn { type Target = rocket_sync_db_pools::Connection; fn deref(&self) -> &Self::Target { &self.0 } } table! { gamenight (id) { id -> diesel::sql_types::Uuid, name -> VarChar, datetime -> VarChar, owner_id -> Uuid, } } table! { known_games (id) { id -> diesel::sql_types::Uuid, name -> VarChar, } } table! { users(id) { id -> diesel::sql_types::Uuid, username -> VarChar, email -> VarChar, role -> crate::schema::RoleMapping, } } table! { pwd(user_id) { user_id -> diesel::sql_types::Uuid, password -> VarChar, } } table! { gamenight_gamelist(gamenight_id, game_id) { gamenight_id -> diesel::sql_types::Uuid, game_id -> diesel::sql_types::Uuid, } } allow_tables_to_appear_in_same_query!(gamenight, known_games,); pub enum DatabaseError { Hash(password_hash::Error), Query(String), } impl From for DatabaseError { fn from(error: diesel::result::Error) -> Self { Self::Query(error.to_string()) } } impl From for DatabaseError { fn from(error: password_hash::Error) -> Self { Self::Hash(error) } } impl std::fmt::Display for DatabaseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { DatabaseError::Hash(err) => write!(f, "{}", err), DatabaseError::Query(err) => write!(f, "{}", err), } } } pub async fn get_all_gamenights(conn: DbConn) -> Result, DatabaseError> { Ok(conn.run(|c| gamenight::table.load::(c) ).await?) } pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNight, game_list: Vec) -> Result { Ok(conn.run(move |c| c.transaction(|| { diesel::insert_into(gamenight::table) .values(&new_gamenight) .execute(c)?; let entries: Vec = game_list.iter().map( |g| GamenightGameListEntry { gamenight_id: new_gamenight.id.clone(), game_id: g.id.clone() } ).collect(); diesel::insert_into(gamenight_gamelist::table) .values(entries) .execute(c) }) ).await?) } pub async fn get_gamenight(conn: &DbConn, game_id: Uuid) -> Result { Ok(conn.run(move |c| gamenight::table.find(game_id).first(c) ).await?) } pub async fn delete_gamenight(conn: &DbConn, game_id: Uuid) -> Result { Ok(conn.run(move |c| diesel::delete( gamenight::table.filter( gamenight::id.eq(game_id) ) ).execute(c) ).await?) } 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 async fn get_all_known_games(conn: &DbConn) -> Result, DatabaseError> { Ok(conn.run(|c| known_games::table.load::(c) ).await?) } pub async fn add_game(conn: &DbConn, game: Game) -> Result { Ok(conn.run(|c| diesel::insert_into(known_games::table) .values(game) .execute(c) ).await?) } pub async fn add_unknown_games(conn: &DbConn, games: &mut Vec) -> Result<(), DatabaseError> { let all_games = get_all_known_games(conn).await?; for game in games.iter_mut() { if !all_games.iter().any(|g| g.name == game.name) { game.id = Uuid::new_v4(); add_game(conn, game.clone()).await?; } } Ok(()) } 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", )), } } pub async fn run_migrations(rocket: Rocket) -> Rocket { // This macro from `diesel_migrations` defines an `embedded_migrations` // module containing a function named `run`. This allows the example to be // run and tested without any outside setup of the database. embed_migrations!(); let conn = DbConn::get_one(&rocket).await.expect("database connection"); conn.run(|c| embedded_migrations::run(c)) .await .expect("can run migrations"); rocket } #[derive(Debug, Serialize, Deserialize, DbEnum, Clone, Copy, PartialEq)] pub enum Role { Admin, User, } #[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(Serialize, Deserialize, Debug, Insertable, Queryable)] #[table_name = "pwd"] struct Pwd { user_id: Uuid, password: String, } #[derive(Serialize, Deserialize, Debug, Queryable, Clone, Insertable)] #[table_name = "known_games"] pub struct Game { pub id: Uuid, pub name: String, } #[derive(Serialize, Deserialize, Debug, Queryable, Insertable)] #[table_name = "gamenight"] pub struct GameNight { pub id: Uuid, pub name: String, pub datetime: String, pub owner_id: Uuid, } #[derive(Serialize, Deserialize, Debug, Queryable, Insertable)] #[table_name="gamenight_gamelist"] pub struct GamenightGameListEntry { pub gamenight_id: Uuid, pub game_id: Uuid } #[derive(Serialize, Deserialize, Debug, Queryable)] pub struct DeleteGameNight { pub game_id: Uuid, } #[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, } #[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, }