diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f456cc9..cd953d3 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -518,6 +518,7 @@ checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -540,12 +541,34 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -567,6 +590,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -584,6 +608,7 @@ dependencies = [ "diesel", "diesel-derive-enum", "diesel_migrations", + "futures", "jsonwebtoken", "local-ip-address", "password-hash", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ca9cf16..8d7c32d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -22,4 +22,5 @@ jsonwebtoken = "8.1" validator = { version = "0.14", features = ["derive"] } rocket_cors = "0.6.0-alpha1" local-ip-address = "0.4" -uuid = { version = "0.8.2", features = ["v4", "serde"] } \ No newline at end of file +uuid = { version = "0.8.2", features = ["v4", "serde"] } +futures = "0.3.21" \ No newline at end of file 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..fc30e6d 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,35 +1,24 @@ -use uuid::Uuid; -use crate::schema; +use crate::schema::gamenight::*; +use crate::schema::users::*; +use crate::schema::DatabaseError; use crate::schema::DbConn; use crate::AppConfig; use chrono::Utc; -use jsonwebtoken::decode; -use jsonwebtoken::encode; -use jsonwebtoken::DecodingKey; -use jsonwebtoken::Validation; -use jsonwebtoken::{EncodingKey, Header}; +use futures::future::join_all; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use rocket::http::Status; -use rocket::request::Outcome; -use rocket::request::{FromRequest, Request}; +use rocket::request::{FromRequest, Outcome, Request}; use rocket::serde::json::{json, Json, Value}; use rocket::State; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use uuid::Uuid; 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 +29,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,20 +56,20 @@ 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, user: Some(UserWithToken { user: user, - jwt: jwt + jwt: jwt, }), gamenights: None, games: None, } } - fn gamenight_response(gamenights: Vec) -> Self { + fn gamenight_response(gamenights: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, @@ -90,7 +79,7 @@ impl ApiResponse { } } - fn games_response(games: Vec) -> Self { + fn games_response(games: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, @@ -110,15 +99,13 @@ 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 { let header = match req.headers().get_one(AUTH_HEADER) { Some(header) => header, - None => { - return Outcome::Forward(()) - } + None => return Outcome::Forward(()), }; if !header.starts_with(BEARER) { @@ -133,25 +120,52 @@ impl<'r> FromRequest<'r> for schema::User { &Validation::default(), ) { Ok(token) => token, - Err(_) => { - return Outcome::Forward(()) - } + Err(_) => return Outcome::Forward(()), }; 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(()) - } + Err(_) => Outcome::Forward(()), + }; } } +#[derive(Debug, Serialize, Deserialize)] +pub struct GamenightOutput { + #[serde(flatten)] + gamenight: Gamenight, + game_list: Vec, + participants: Vec, +} + #[get("/gamenights")] -pub async fn gamenights(conn: DbConn, _user: schema::User) -> ApiResponseVariant { - match schema::get_all_gamenights(conn).await { - Ok(gamenights) => ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(gamenights))), - Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) +pub async fn gamenights(conn: DbConn, _user: User) -> ApiResponseVariant { + let gamenights = match get_all_gamenights(&conn).await { + Ok(result) => result, + Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), + }; + + let conn_ref = &conn; + + let game_results: Result, DatabaseError> = + join_all(gamenights.iter().map(|gn| async move { + let games = get_games_of_gamenight(conn_ref, gn.id).await?; + let participants = load_participants(conn_ref, gn.id).await?; + Ok(GamenightOutput { + gamenight: gn.clone(), + game_list: games, + participants: participants, + }) + })) + .await + .into_iter() + .collect(); + + match game_results { + Ok(result) => ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(result))), + Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } @@ -161,21 +175,20 @@ pub async fn gamenights_unauthorized() -> ApiResponseVariant { } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GameNightInput { +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 { - - fn into(self) -> schema::GameNight { - schema::GameNight { +impl Into for GamenightInput { + fn into(self) -> Gamenight { + Gamenight { id: Uuid::new_v4(), name: self.name, datetime: self.datetime, - owner_id: self.owner_id.unwrap() + owner_id: self.owner_id.unwrap(), } } } @@ -183,22 +196,33 @@ impl Into for GameNightInput { #[post("/gamenights", format = "application/json", data = "")] pub async fn gamenights_post_json( conn: DbConn, - user: schema::User, - gamenight_json: Json, + user: User, + gamenight_json: Json, ) -> ApiResponseVariant { let mut gamenight = gamenight_json.into_inner(); gamenight.owner_id = Some(user.id); 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()))) + Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), }; - match schema::insert_gamenight(conn, gamenight.clone().into(), mutable_game_list).await { + let gamenight_id = match insert_gamenight(&conn, gamenight.clone().into(), mutable_game_list) + .await + { + Ok(id) => id, + Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), + }; + + let participant = GamenightParticipantsEntry { + gamenight_id: gamenight_id, + user_id: user.id, + }; + match insert_participant(&conn, participant).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), - Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))) + Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } @@ -207,44 +231,46 @@ pub async fn gamenights_post_json_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } -#[delete("/gamenights", format = "application/json", data = "")] +#[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 { - return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + 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)) + 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 { - return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + 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)) + return ApiResponseVariant::Value(json!(ApiResponse::SUCCES)); } - }, - Err(error) => return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + } + Err(error) => { + return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + } } ApiResponseVariant::Status(Status::Unauthorized) } - #[delete("/gamenights", rank = 2)] pub async fn gamenights_delete_json_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[post("/register", format = "application/json", data = "")] -pub async fn register_post_json( - conn: DbConn, - register_json: Json, -) -> ApiResponseVariant { +pub async fn register_post_json(conn: DbConn, register_json: Json) -> ApiResponseVariant { let register = register_json.into_inner(); let register_clone = register.clone(); match conn @@ -257,7 +283,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 +293,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 { @@ -298,7 +324,9 @@ pub async fn login_post_json( &my_claims, &EncodingKey::from_secret(secret.as_bytes()), ) { - Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))), + Ok(token) => { + ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))) + } Err(error) => { ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } @@ -308,10 +336,10 @@ 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()))) + Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))), } } @@ -320,3 +348,57 @@ pub async fn games_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } +#[get( + "/participants", + format = "application/json", + data = "" +)] +pub async fn get_participants( + conn: DbConn, + _user: User, + gamenight_id_json: Json, +) -> ApiResponseVariant { + match load_participants(&conn, gamenight_id_json.into_inner().gamenight_id).await { + Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), + Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))), + } +} + +#[get("/participants", rank = 2)] +pub async fn get_participants_unauthorized() -> ApiResponseVariant { + ApiResponseVariant::Status(Status::Unauthorized) +} + +#[post("/participants", format = "application/json", data = "")] +pub async fn post_participants( + conn: DbConn, + _user: User, + entry_json: Json, +) -> ApiResponseVariant { + match insert_participant(&conn, entry_json.into_inner()).await { + Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), + Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))), + } +} + +#[post("/participants", rank = 2)] +pub async fn post_participants_unauthorized() -> ApiResponseVariant { + ApiResponseVariant::Status(Status::Unauthorized) +} + +#[delete("/participants", format = "application/json", data = "")] +pub async fn delete_participants( + conn: DbConn, + _user: User, + entry_json: Json, +) -> ApiResponseVariant { + match remove_participant(&conn, entry_json.into_inner()).await { + Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), + Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))), + } +} + +#[delete("/participants", rank = 2)] +pub async fn delete_participants_unauthorized() -> ApiResponseVariant { + ApiResponseVariant::Status(Status::Unauthorized) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 3b4215f..2e1c2b0 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -46,13 +46,7 @@ async fn rocket() -> _ { .attach(AdHoc::on_ignite("Run Migrations", schema::run_migrations)) .attach(AdHoc::config::()) .attach(site::make_cors()) - .mount( - "/", - routes![ - site::index, - site::files - ], - ) + .mount("/", routes![site::index, site::files]) .mount( "/api", routes![ @@ -66,6 +60,12 @@ async fn rocket() -> _ { api::gamenights_delete_json_unauthorized, api::games, api::games_unauthorized, + api::get_participants, + api::get_participants_unauthorized, + api::post_participants, + api::post_participants_unauthorized, + api::delete_participants, + api::delete_participants_unauthorized, ], ); 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..3cc1c0a --- /dev/null +++ b/backend/src/schema/gamenight.rs @@ -0,0 +1,219 @@ +use crate::schema::users::{users, User}; +use crate::schema::{DatabaseError, DbConn}; +use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde::{Deserialize, Serialize}; +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, Clone)] +#[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, +} + +#[derive(Serialize, Deserialize, Debug, Queryable)] +pub struct GamenightId { + pub gamenight_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::<_, DatabaseError, _>(|| { + let id: Uuid = diesel::insert_into(gamenight::table) + .values(&new_gamenight) + .returning(gamenight::id) + .get_result(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)?; + + Ok(id) + }) + }) + .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 get_games_of_gamenight( + conn: &DbConn, + gamenight_id: Uuid, +) -> Result, DatabaseError> { + Ok(conn + .run::<_, Result, _>>(move |c| { + let linked_game_ids: Vec = gamenight_gamelist::table + .filter(gamenight_gamelist::gamenight_id.eq(gamenight_id)) + .load::(c)?; + + linked_game_ids + .iter() + .map(|l| { + known_games::table + .filter(known_games::id.eq(l.game_id)) + .first::(c) + }) + .collect() + }) + .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 load_participants( + conn: &DbConn, + gamenight_id: Uuid, +) -> Result, DatabaseError> { + Ok(conn + .run::<_, Result, _>>(move |c| { + let linked_participants = gamenight_participants::table + .filter(gamenight_participants::gamenight_id.eq(gamenight_id)) + .load::(c)?; + linked_participants + .iter() + .map(|l| { + users::table + .filter(users::id.eq(l.user_id)) + .first::(c) + }) + .collect() + }) + .await?) +} + +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?) +} diff --git a/backend/src/schema/mod.rs b/backend/src/schema/mod.rs new file mode 100644 index 0000000..60598b5 --- /dev/null +++ b/backend/src/schema/mod.rs @@ -0,0 +1,57 @@ +pub mod gamenight; +pub mod users; + +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..a29ca50 --- /dev/null +++ b/backend/src/schema/users.rs @@ -0,0 +1,191 @@ +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", + )), + } +} diff --git a/backend/src/site.rs b/backend/src/site.rs index 7f47ca2..0d8193d 100644 --- a/backend/src/site.rs +++ b/backend/src/site.rs @@ -1,20 +1,19 @@ +use local_ip_address::local_ip; use rocket::fs::NamedFile; use rocket::http::Method; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions}; use std::io; use std::path::{Path, PathBuf}; -use local_ip_address::local_ip; - pub fn make_cors() -> Cors { let allowed_origins = AllowedOrigins::some_exact(&[ "http://localhost:3000", "http://127.0.0.1:3000", - &format!("http://{}:8000",local_ip().unwrap())[..], + &format!("http://{}:8000", local_ip().unwrap())[..], "http://localhost:8000", "http://0.0.0.0:8000", ]); - + CorsOptions { allowed_origins, allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(), // 1. diff --git a/frontend/src/App.js b/frontend/src/App.js index f1f8416..211c285 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,8 +2,9 @@ import './App.css'; import React, { useState, useEffect } from 'react'; import MenuBar from './components/MenuBar'; import Login from './components/Login'; -import Gamenights from './components/Gamenights' -import AddGameNight from './components/AddGameNight' +import Gamenights from './components/Gamenights'; +import AddGameNight from './components/AddGameNight'; +import Gamenight from './components/Gamenight'; const localStorageUserKey = 'user'; @@ -13,6 +14,7 @@ function App() { const [gamenights, setGamenights] = useState([]); const [flashData, setFlashData] = useState({}); const [games, setGames] = useState([]); + const [activeGamenight, setActiveGamenight] = useState(null); const handleLogin = (input) => { const requestOptions = { @@ -48,6 +50,10 @@ function App() { setUser({...user}); }; + const dismissActiveGamenight = () => { + setActiveGamenight(null); + }; + useEffect(() => { if (user !== null) { const requestOptions = { @@ -101,11 +107,29 @@ function App() { ); } else { + + let mainview; + if(activeGamenight === null) { + mainview = <> + + setActiveGamenight(g)}/> + + } else { + mainview = + } + return ( <> - - + {mainview} ); } diff --git a/frontend/src/components/AddGameNight.jsx b/frontend/src/components/AddGameNight.jsx index 53667d9..9349c68 100644 --- a/frontend/src/components/AddGameNight.jsx +++ b/frontend/src/components/AddGameNight.jsx @@ -2,21 +2,13 @@ import React, { useEffect, useState } from 'react'; import DateTime from 'react-datetime'; import GameAdder from './GameAdder'; -import Autocomplete from '@mui/material/Autocomplete'; - import "react-datetime/css/react-datetime.css"; function AddGameNight(props) { const [expanded, setExpanded] = useState(false); const [gameName, setGameName] = useState(""); const [date, setDate] = useState(Date.now()); - const [gameList, setGameList] = useState([]); - - const emptyUuid = "00000000-0000-0000-0000-000000000000"; - - //temp hack: - props.games = [{id: emptyUuid, name: "mystic vale"}, {id: emptyUuid, name: "Crew"}]; - + const [gameList, setGameList] = useState([]); const handleNameChange = (event) => { setGameName(event.target.value); diff --git a/frontend/src/components/Gamenight.jsx b/frontend/src/components/Gamenight.jsx new file mode 100644 index 0000000..2664163 --- /dev/null +++ b/frontend/src/components/Gamenight.jsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +function Gamenight(props) { + + let games = props.gamenight.game_list.map(g => + ( +
  • + {g.name} +
  • + ) + ); + + let participants = props.gamenight.participants.map(p => + ( +
  • + {p.username} +
  • + ) + ) + + return ( +
    +

    {props.gamenight.name}

    + + {props.gamenight.datetime} +

    Games:

    +
      + {games} +
    +

    Participants:

    +
      + {participants} +
    +
    + ) +} + +export default Gamenight diff --git a/frontend/src/components/Gamenights.jsx b/frontend/src/components/Gamenights.jsx index 3a5d35a..bf54a28 100644 --- a/frontend/src/components/Gamenights.jsx +++ b/frontend/src/components/Gamenights.jsx @@ -29,16 +29,16 @@ function Gamenights(props) { }) .then(() => props.refetchGamenights()); } - } + } let gamenights = props.gamenights.map(g => ( -
  • +
  • props.onSelectGamenight(g)}> {g.name} {(props.user.id === g.owner_id || props.user.role === "Admin") && - + }
  • )