diff --git a/backend/migrations/2022-05-28-142526_gamenight participants/down.sql b/backend/migrations/2022-05-28-142526_gamenight participants/down.sql new file mode 100644 index 0000000..e32c462 --- /dev/null +++ b/backend/migrations/2022-05-28-142526_gamenight participants/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +drop table gamenight_participants; \ No newline at end of file diff --git a/backend/migrations/2022-05-28-142526_gamenight participants/up.sql b/backend/migrations/2022-05-28-142526_gamenight participants/up.sql new file mode 100644 index 0000000..1115fc9 --- /dev/null +++ b/backend/migrations/2022-05-28-142526_gamenight participants/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +create table gamenight_participants ( + gamenight_id UUID NOT NULL, + user_id UUID NOT NULL, + CONSTRAINT FK_gamenight_id FOREIGN KEY (gamenight_id) REFERENCES gamenight(id) ON DELETE CASCADE, + CONSTRAINT FK_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(gamenight_id, user_id) +) \ No newline at end of file diff --git a/backend/src/api.rs b/backend/src/api.rs index 5f7a959..17608fd 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,7 +1,8 @@ -use uuid::Uuid; -use crate::schema; +use crate::schema::users::*; +use crate::schema::gamenight::*; use crate::schema::DbConn; use crate::AppConfig; +use uuid::Uuid; use chrono::Utc; use jsonwebtoken::decode; use jsonwebtoken::encode; @@ -20,16 +21,7 @@ use validator::ValidateArgs; #[derive(Debug, Responder)] pub enum ApiResponseVariant { Status(Status), - // Redirect(Redirect), Value(Value), - // Flash(Flash) -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserWithToken { - #[serde(flatten)] - pub user: schema::User, - pub jwt: String, } #[derive(Serialize, Deserialize, Debug)] @@ -40,9 +32,9 @@ struct ApiResponse { #[serde(skip_serializing_if = "Option::is_none")] user: Option, #[serde(skip_serializing_if = "Option::is_none")] - gamenights: Option>, + gamenights: Option>, #[serde(skip_serializing_if = "Option::is_none")] - games: Option>, + games: Option>, } impl ApiResponse { @@ -67,7 +59,7 @@ impl ApiResponse { } } - fn login_response(user: schema::User, jwt: String) -> Self { + fn login_response(user: User, jwt: String) -> Self { Self { result: Self::SUCCES_RESULT, message: None, @@ -80,7 +72,7 @@ impl ApiResponse { } } - fn gamenight_response(gamenights: Vec) -> Self { + fn gamenight_response(gamenights: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, @@ -90,7 +82,7 @@ impl ApiResponse { } } - fn games_response(games: Vec) -> Self { + fn games_response(games: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, @@ -110,7 +102,7 @@ const AUTH_HEADER: &str = "Authorization"; const BEARER: &str = "Bearer "; #[rocket::async_trait] -impl<'r> FromRequest<'r> for schema::User { +impl<'r> FromRequest<'r> for User { type Error = ApiError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -140,7 +132,7 @@ impl<'r> FromRequest<'r> for schema::User { let id = token.claims.uid; let conn = req.guard::().await.unwrap(); - return match schema::get_user(conn, id).await { + return match get_user(conn, id).await { Ok(o) => Outcome::Success(o), Err(_) => Outcome::Forward(()) } @@ -148,8 +140,8 @@ impl<'r> FromRequest<'r> for schema::User { } #[get("/gamenights")] -pub async fn gamenights(conn: DbConn, _user: schema::User) -> ApiResponseVariant { - match schema::get_all_gamenights(conn).await { +pub async fn gamenights(conn: DbConn, _user: User) -> ApiResponseVariant { + match get_all_gamenights(conn).await { Ok(gamenights) => ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(gamenights))), Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } @@ -165,13 +157,13 @@ pub struct GameNightInput { pub name: String, pub datetime: String, pub owner_id: Option, - pub game_list: Vec, + pub game_list: Vec, } -impl Into for GameNightInput { +impl Into for GameNightInput { - fn into(self) -> schema::GameNight { - schema::GameNight { + fn into(self) -> GameNight { + GameNight { id: Uuid::new_v4(), name: self.name, datetime: self.datetime, @@ -183,7 +175,7 @@ impl Into for GameNightInput { #[post("/gamenights", format = "application/json", data = "")] pub async fn gamenights_post_json( conn: DbConn, - user: schema::User, + user: User, gamenight_json: Json, ) -> ApiResponseVariant { let mut gamenight = gamenight_json.into_inner(); @@ -191,12 +183,12 @@ pub async fn gamenights_post_json( let mut mutable_game_list = gamenight.game_list.clone(); - match schema::add_unknown_games(&conn, &mut mutable_game_list).await { + match add_unknown_games(&conn, &mut mutable_game_list).await { Ok(_) => (), Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))) }; - match schema::insert_gamenight(conn, gamenight.clone().into(), mutable_game_list).await { + match insert_gamenight(conn, gamenight.clone().into(), mutable_game_list).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))) } @@ -210,20 +202,20 @@ pub async fn gamenights_post_json_unauthorized() -> ApiResponseVariant { #[delete("/gamenights", format = "application/json", data = "")] pub async fn gamenights_delete_json( conn: DbConn, - user: schema::User, - delete_gamenight_json: Json + user: User, + delete_gamenight_json: Json ) -> ApiResponseVariant { - if user.role == schema::Role::Admin { - if let Err(error) = schema::delete_gamenight(&conn, delete_gamenight_json.game_id).await { + if user.role == Role::Admin { + if let Err(error) = delete_gamenight(&conn, delete_gamenight_json.game_id).await { return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } return ApiResponseVariant::Value(json!(ApiResponse::SUCCES)) } - match schema::get_gamenight(&conn, delete_gamenight_json.game_id).await { + match get_gamenight(&conn, delete_gamenight_json.game_id).await { Ok(gamenight) => { if user.id == gamenight.owner_id { - if let Err(error) = schema::delete_gamenight(&conn, delete_gamenight_json.game_id).await { + if let Err(error) = delete_gamenight(&conn, delete_gamenight_json.game_id).await { return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } return ApiResponseVariant::Value(json!(ApiResponse::SUCCES)) @@ -243,7 +235,7 @@ pub async fn gamenights_delete_json_unauthorized() -> ApiResponseVariant { #[post("/register", format = "application/json", data = "")] pub async fn register_post_json( conn: DbConn, - register_json: Json, + register_json: Json, ) -> ApiResponseVariant { let register = register_json.into_inner(); let register_clone = register.clone(); @@ -257,7 +249,7 @@ pub async fn register_post_json( } } - match schema::insert_user(conn, register).await { + match insert_user(conn, register).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } @@ -267,16 +259,16 @@ pub async fn register_post_json( struct Claims { exp: i64, uid: Uuid, - role: schema::Role, + role: Role, } #[post("/login", format = "application/json", data = "")] pub async fn login_post_json( conn: DbConn, config: &State, - login_json: Json, + login_json: Json, ) -> ApiResponseVariant { - match schema::login(conn, login_json.into_inner()).await { + match login(conn, login_json.into_inner()).await { Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), Ok(login_result) => { if !login_result.result { @@ -308,8 +300,8 @@ pub async fn login_post_json( } #[get("/games")] -pub async fn games(conn: DbConn, _user: schema::User) -> ApiResponseVariant { - match schema::get_all_known_games(&conn).await { +pub async fn games(conn: DbConn, _user: User) -> ApiResponseVariant { + match get_all_known_games(&conn).await { Ok(games) => ApiResponseVariant::Value(json!(ApiResponse::games_response(games))), Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } diff --git a/backend/src/schema.rs b/backend/src/schema.rs deleted file mode 100644 index 2da917d..0000000 --- a/backend/src/schema.rs +++ /dev/null @@ -1,360 +0,0 @@ -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, -} diff --git a/backend/src/schema/gamenight.rs b/backend/src/schema/gamenight.rs new file mode 100644 index 0000000..a69d190 --- /dev/null +++ b/backend/src/schema/gamenight.rs @@ -0,0 +1,161 @@ +use crate::schema::{DatabaseError, DbConn}; +use serde::{Deserialize, Serialize}; +use diesel::{QueryDsl, RunQueryDsl, Connection, ExpressionMethods}; +use uuid::Uuid; + +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! { + gamenight_gamelist(gamenight_id, game_id) { + gamenight_id -> diesel::sql_types::Uuid, + game_id -> diesel::sql_types::Uuid, + } +} + +table! { + gamenight_participants(gamenight_id, user_id) { + gamenight_id -> diesel::sql_types::Uuid, + user_id -> diesel::sql_types::Uuid, + } +} + +#[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, Insertable, Identifiable)] +#[table_name="gamenight_participants"] +#[primary_key(gamenight_id, user_id)] +pub struct GamenightParticipantsEntry { + pub gamenight_id: Uuid, + pub user_id: Uuid +} + +#[derive(Serialize, Deserialize, Debug, Queryable)] +pub struct DeleteGameNight { + pub game_id: Uuid, +} + + +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 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 async fn insert_participant(conn: &DbConn, participant: GamenightParticipantsEntry) -> Result { + Ok(conn.run(move |c| + diesel::insert_into(gamenight_participants::table) + .values(&participant) + .execute(c) + ).await?) +} + +impl From for (Uuid, Uuid) { + + fn from(entry: GamenightParticipantsEntry) -> Self { + (entry.gamenight_id, entry.user_id) + } +} + +pub async fn remove_participant(conn: &DbConn, participant: GamenightParticipantsEntry) -> Result { + Ok(conn.run(move |c| + diesel::delete(&participant) + .execute(c) + ).await?) +} \ No newline at end of file diff --git a/backend/src/schema/mod.rs b/backend/src/schema/mod.rs new file mode 100644 index 0000000..c4170da --- /dev/null +++ b/backend/src/schema/mod.rs @@ -0,0 +1,60 @@ +pub mod users; +pub mod gamenight; + +use rocket::{Build, Rocket}; +use rocket_sync_db_pools::database; +use std::ops::Deref; + +#[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 + } +} + +pub enum DatabaseError { + Hash(password_hash::Error), + Query(String), +} + +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 +} + +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), + } + } +} + + + diff --git a/backend/src/schema/users.rs b/backend/src/schema/users.rs new file mode 100644 index 0000000..4a1a1cd --- /dev/null +++ b/backend/src/schema/users.rs @@ -0,0 +1,191 @@ +use validator::{Validate, ValidationError}; +use uuid::Uuid; +use diesel::{QueryDsl, RunQueryDsl, Connection, ExpressionMethods}; +use argon2::password_hash::SaltString; +use argon2::PasswordHash; +use argon2::PasswordVerifier; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher}, + Argon2, +}; +use diesel_derive_enum::DbEnum; +use serde::{Deserialize, Serialize}; +use crate::schema::{DbConn, DatabaseError}; + +#[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", + )), + } +} \ No newline at end of file