diff --git a/backend/src/api.rs b/backend/src/api.rs index f655e06..4be800c 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,36 +1,36 @@ -use validator::ValidateArgs; -use crate::AppConfig; -use rocket::request::Outcome; -use jsonwebtoken::decode; +use crate::schema; use crate::schema::DbConn; +use crate::AppConfig; +use chrono::Utc; +use jsonwebtoken::decode; +use jsonwebtoken::encode; use jsonwebtoken::DecodingKey; use jsonwebtoken::Validation; -use rocket::State; -use chrono::Utc; -use jsonwebtoken::{Header, EncodingKey}; -use crate::schema; -use std::borrow::Cow; -use jsonwebtoken::encode; -use rocket::serde::json::{Json, json, Value}; +use jsonwebtoken::{EncodingKey, Header}; use rocket::http::Status; -use rocket::request::{self, Request, FromRequest}; -use rocket::outcome::Outcome::{Success, Failure}; -use serde::{Serialize, Deserialize}; +use rocket::outcome::Outcome::{Failure, Success}; +use rocket::request::Outcome; +use rocket::request::{self, FromRequest, Request}; +use rocket::serde::json::{json, Json, Value}; +use rocket::State; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use validator::ValidateArgs; pub struct Referer(String); #[derive(Debug)] pub enum ReferrerError { Missing, - MoreThanOne + MoreThanOne, } #[derive(Debug, Responder)] pub enum ApiResponseVariant { Status(Status), -// Redirect(Redirect), + // Redirect(Redirect), Value(Value), -// Flash(Flash) + // Flash(Flash) } #[rocket::async_trait] @@ -38,7 +38,7 @@ impl<'r> FromRequest<'r> for Referer { type Error = ReferrerError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { - let referers : Vec<_> = req.headers().get("Referer").collect(); + let referers: Vec<_> = req.headers().get("Referer").collect(); match referers.len() { 0 => Failure((Status::BadRequest, ReferrerError::Missing)), 1 => Success(Referer(referers[0].to_string())), @@ -51,9 +51,9 @@ impl<'r> FromRequest<'r> for Referer { struct ApiResponse { result: Cow<'static, str>, #[serde(skip_serializing_if = "Option::is_none")] - message: Option::>, + message: Option>, #[serde(skip_serializing_if = "Option::is_none")] - jwt: Option::> + jwt: Option>, } impl ApiResponse { @@ -63,14 +63,14 @@ impl ApiResponse { const SUCCES: Self = Self { result: Self::SUCCES_RESULT, message: None, - jwt: None + jwt: None, }; fn error(message: String) -> Self { - Self { + Self { result: Self::FAILURE_RESULT, message: Some(Cow::Owned(message)), - jwt: None + jwt: None, } } @@ -78,7 +78,7 @@ impl ApiResponse { Self { result: Self::SUCCES_RESULT, message: None, - jwt: Some(Cow::Owned(jwt)) + jwt: Some(Cow::Owned(jwt)), } } } @@ -98,23 +98,40 @@ impl<'r> FromRequest<'r> for schema::User { async fn from_request(req: &'r Request<'_>) -> Outcome { let header = match req.headers().get_one(AUTH_HEADER) { Some(header) => header, - None => return Outcome::Failure((Status::BadRequest, ApiError::RequestError("No authorization header found".to_string()))) + None => { + return Outcome::Failure(( + Status::BadRequest, + ApiError::RequestError("No authorization header found".to_string()), + )) + } }; if !header.starts_with(BEARER) { - return Outcome::Failure((Status::BadRequest, ApiError::RequestError("Invalid Authorization header.".to_string()))) + return Outcome::Failure(( + Status::BadRequest, + ApiError::RequestError("Invalid Authorization header.".to_string()), + )); }; let app_config = req.guard::<&State>().await.unwrap().inner(); let jwt = header.trim_start_matches(BEARER).to_owned(); - let token = match decode::(&jwt, &DecodingKey::from_secret(app_config.jwt_secret.as_bytes()), &Validation::default()) { + let token = match decode::( + &jwt, + &DecodingKey::from_secret(app_config.jwt_secret.as_bytes()), + &Validation::default(), + ) { Ok(token) => token, - Err(error) => return Outcome::Failure((Status::BadRequest, ApiError::RequestError(error.to_string()))) + Err(error) => { + return Outcome::Failure(( + Status::BadRequest, + ApiError::RequestError(error.to_string()), + )) + } }; let id = token.claims.uid; - + let conn = req.guard::().await.unwrap(); - return Outcome::Success(schema::get_user(conn, id).await) + return Outcome::Success(schema::get_user(conn, id).await); } } @@ -123,38 +140,45 @@ pub async fn gamenights(conn: DbConn, user: Option) -> ApiResponse if user.is_some() { let gamenights = schema::get_all_gamenights(conn).await; ApiResponseVariant::Value(json!(gamenights)) - } - else { + } else { ApiResponseVariant::Status(Status::Unauthorized) } } #[post("/gamenight", format = "application/json", data = "")] -pub async fn gamenight_post_json(conn: DbConn, user: Option, gamenight_json: Json) -> ApiResponseVariant { +pub async fn gamenight_post_json( + conn: DbConn, + user: Option, + gamenight_json: Json, +) -> ApiResponseVariant { if user.is_some() { schema::insert_gamenight(conn, gamenight_json.into_inner()).await; ApiResponseVariant::Value(json!(ApiResponse::SUCCES)) - } - else { + } else { 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.run(move |c| { - register_clone.validate_args((c,c)) - }).await { + match conn + .run(move |c| register_clone.validate_args((c, c))) + .await + { Ok(()) => (), - Err(error) => return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + Err(error) => { + return ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + } } - + match schema::insert_user(conn, register).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()))), } } @@ -166,24 +190,37 @@ struct Claims { } #[post("/login", format = "application/json", data = "")] -pub async fn login_post_json(conn: DbConn, config: &State, login_json: Json) -> ApiResponseVariant { - +pub async fn login_post_json( + conn: DbConn, + config: &State, + login_json: Json, +) -> ApiResponseVariant { match schema::login(conn, login_json.into_inner()).await { Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), Ok(login_result) => { - + if !login_result.result { + return ApiResponseVariant::Value(json!(ApiResponse::error(String::from( + "username and password didn't match" + )))); + } + let my_claims = Claims { exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(), uid: login_result.id.unwrap(), - role: login_result.role.unwrap() + role: login_result.role.unwrap(), }; - + let secret = &config.inner().jwt_secret; - match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(secret.as_bytes())) - { + match encode( + &Header::default(), + &my_claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) { Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(token))), - Err(error) => ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + Err(error) => { + ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) + } } } } -} \ No newline at end of file +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 80233d2..f46caf8 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,8 +1,17 @@ -#[macro_use] extern crate rocket; -#[macro_use] extern crate diesel_migrations; -#[macro_use] extern crate diesel; +#[macro_use] +extern crate rocket; +#[macro_use] +extern crate diesel_migrations; +#[macro_use] +extern crate diesel; -use rocket::{fairing::AdHoc, figment::{Figment, providers::{Serialized, Toml, Env, Format}, Profile}}; +use rocket::{ + fairing::AdHoc, + figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, Profile, + }, +}; use rocket_dyn_templates::Template; use serde::{Deserialize, Serialize}; @@ -12,12 +21,14 @@ mod site; #[derive(Debug, Deserialize, Serialize)] pub struct AppConfig { - jwt_secret: String + jwt_secret: String, } impl Default for AppConfig { fn default() -> AppConfig { - AppConfig { jwt_secret: String::from("secret") } + AppConfig { + jwt_secret: String::from("secret"), + } } } @@ -34,13 +45,17 @@ fn rocket() -> _ { .attach(Template::fairing()) .attach(AdHoc::on_ignite("Run Migrations", schema::run_migrations)) .attach(AdHoc::config::()) - .mount("/", routes![site::index, site::gamenights, - site::add_game_night, site::register]) - .mount("/api", routes![ - api::gamenights, api::gamenight_post_json, - api::register_post_json, - api::login_post_json - ]); - + .attach(site::make_cors()) + .mount("/", routes![site::index, site::files]) + .mount( + "/api", + routes![ + api::gamenights, + api::gamenight_post_json, + api::register_post_json, + api::login_post_json + ], + ); + rocket } diff --git a/backend/src/schema.rs b/backend/src/schema.rs index 6d5aaa3..69e411e 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -1,24 +1,21 @@ -use diesel::dsl::count; -use std::ops::Deref; -use argon2::PasswordVerifier; -use argon2::PasswordHash; -use diesel_derive_enum::DbEnum; -use crate::diesel::QueryDsl; use crate::diesel::BoolExpressionMethods; -use crate::diesel::ExpressionMethods; use crate::diesel::Connection; -use rocket_sync_db_pools::database; -use serde::{Serialize, Deserialize}; -use rocket::{Rocket, Build}; -use diesel::RunQueryDsl; -use argon2::{ - password_hash::{ - rand_core::OsRng, - PasswordHasher - }, - Argon2 -}; +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::dsl::count; +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")] @@ -66,30 +63,25 @@ table! { } } -allow_tables_to_appear_in_same_query!( - gamenight, - known_games, -); +allow_tables_to_appear_in_same_query!(gamenight, known_games,); pub enum DatabaseError { Hash(password_hash::Error), - Query(String) + Query(String), } 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) - } + DatabaseError::Query(err) => write!(f, "{}", err), + } } } -pub async fn get_all_gamenights(conn: DbConn) -> Vec:: { - conn.run(|c| { - gamenight::table.load::(c).unwrap() - }).await +pub async fn get_all_gamenights(conn: DbConn) -> Vec { + conn.run(|c| gamenight::table.load::(c).unwrap()) + .await } pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNightNoId) -> () { @@ -98,7 +90,8 @@ pub async fn insert_gamenight(conn: DbConn, new_gamenight: GameNightNoId) -> () .values(new_gamenight) .execute(c) .unwrap() - }).await; + }) + .await; } pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), DatabaseError> { @@ -107,29 +100,41 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), Databas let argon2 = Argon2::default(); let password_hash = match argon2.hash_password(new_user.password.as_bytes(), &salt) { - Ok(hash) => hash.to_string(), - Err(error) => return Err(DatabaseError::Hash(error)) + Ok(hash) => hash.to_string(), + Err(error) => return Err(DatabaseError::Hash(error)), }; - match conn.run(move |c| { - c.transaction(|| { - diesel::insert_into(user::table) - .values((user::username.eq(&new_user.username), user::email.eq(&new_user.email), user::role.eq(Role::User))) - .execute(c)?; - - let ids : Vec:: = match user::table - .filter(user::username.eq(&new_user.username).and(user::email.eq(&new_user.email))) - .select(user::id) - .get_results(c) { + match conn + .run(move |c| { + c.transaction(|| { + diesel::insert_into(user::table) + .values(( + user::username.eq(&new_user.username), + user::email.eq(&new_user.email), + user::role.eq(Role::User), + )) + .execute(c)?; + + let ids: Vec = match user::table + .filter( + user::username + .eq(&new_user.username) + .and(user::email.eq(&new_user.email)), + ) + .select(user::id) + .get_results(c) + { Ok(id) => id, - Err(e) => return Err(e) + Err(e) => return Err(e), }; - diesel::insert_into(pwd::table) - .values((pwd::id.eq(ids[0]), pwd::password.eq(&password_hash))) - .execute(c) + diesel::insert_into(pwd::table) + .values((pwd::id.eq(ids[0]), pwd::password.eq(&password_hash))) + .execute(c) + }) }) - }).await { + .await + { Err(e) => Err(DatabaseError::Query(e.to_string())), _ => Ok(()), } @@ -137,83 +142,94 @@ pub async fn insert_user(conn: DbConn, new_user: Register) -> Result<(), Databas pub async fn login(conn: DbConn, login: Login) -> Result { conn.run(move |c| -> Result { - let id : i32 = match user::table + let id: i32 = match user::table .filter(user::username.eq(&login.username)) .or_filter(user::email.eq(&login.username)) .select(user::id) - .get_results(c) { - Ok(id) => id[0], - Err(error) => return Err(DatabaseError::Query(error.to_string())) - }; - - let pwd : String = match pwd::table + .get_results(c) + { + Ok(id) => id[0], + Err(error) => return Err(DatabaseError::Query(error.to_string())), + }; + + let pwd: String = match pwd::table .filter(pwd::id.eq(id)) .select(pwd::password) - .get_results::(c) { - Ok(pwd) => pwd[0].clone(), - Err(error) => return Err(DatabaseError::Query(error.to_string())) - }; + .get_results::(c) + { + Ok(pwd) => pwd[0].clone(), + Err(error) => return Err(DatabaseError::Query(error.to_string())), + }; let parsed_hash = match PasswordHash::new(&pwd) { Ok(hash) => hash, - Err(error) => return Err(DatabaseError::Hash(error)) + Err(error) => return Err(DatabaseError::Hash(error)), }; - if Argon2::default().verify_password(&login.password.as_bytes(), &parsed_hash).is_ok() { - - let role : Role = match user::table + if Argon2::default() + .verify_password(&login.password.as_bytes(), &parsed_hash) + .is_ok() + { + let role: Role = match user::table .filter(user::id.eq(id)) .select(user::role) - .get_results::(c) { - Ok(role) => role[0].clone(), - Err(error) => return Err(DatabaseError::Query(error.to_string())) - }; + .get_results::(c) + { + Ok(role) => role[0].clone(), + Err(error) => return Err(DatabaseError::Query(error.to_string())), + }; Ok(LoginResult { result: true, id: Some(id), - role: Some(role) + role: Some(role), }) - } - else { + } else { Ok(LoginResult { result: false, id: None, role: None, }) } - }).await + }) + .await } pub async fn get_user(conn: DbConn, id: i32) -> User { - conn.run(move |c| { - user::table - .filter(user::id.eq(id)) - .first(c) - .unwrap() - }).await + conn.run(move |c| user::table.filter(user::id.eq(id)).first(c).unwrap()) + .await } -pub fn unique_username(username: &String, conn: &diesel::SqliteConnection) -> Result<(), ValidationError> { +pub fn unique_username( + username: &String, + conn: &diesel::SqliteConnection, +) -> Result<(), ValidationError> { match user::table .select(count(user::username)) .filter(user::username.eq(username)) - .execute(conn) { - Ok(0) => Ok(()), - Ok(_) => Err(ValidationError::new("User already exists")), - Err(_) => Err(ValidationError::new("Database error while validating user")) - } + .execute(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::SqliteConnection) -> Result<(), ValidationError> { +pub fn unique_email( + email: &String, + conn: &diesel::SqliteConnection, +) -> Result<(), ValidationError> { match user::table .select(count(user::email)) .filter(user::email.eq(email)) - .execute(conn) { - Ok(0) => Ok(()), - Ok(_) => Err(ValidationError::new("email already exists")), - Err(_) => Err(ValidationError::new("Database error while validating email")) - } + .execute(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 { @@ -223,7 +239,9 @@ pub async fn run_migrations(rocket: Rocket) -> Rocket { embed_migrations!(); let conn = DbConn::get_one(&rocket).await.expect("database connection"); - conn.run(|c| embedded_migrations::run(c)).await.expect("can run migrations"); + conn.run(|c| embedded_migrations::run(c)) + .await + .expect("can run migrations"); rocket } @@ -235,45 +253,51 @@ pub enum Role { } #[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] -#[table_name="user"] +#[table_name = "user"] pub struct User { pub id: i32, pub username: String, pub email: String, - pub role: Role + pub role: Role, } #[derive(Serialize, Deserialize, Debug, FromForm, Insertable)] -#[table_name="known_games"] +#[table_name = "known_games"] pub struct GameNoId { - pub game : String, + pub game: String, } #[derive(Serialize, Deserialize, Debug, FromForm, Queryable)] pub struct Game { pub id: i32, - pub game : String, + pub game: String, } #[derive(Serialize, Deserialize, Debug, FromForm, Insertable)] -#[table_name="gamenight"] +#[table_name = "gamenight"] pub struct GameNightNoId { - pub game : String, - pub datetime : String, + pub game: String, + pub datetime: String, } #[derive(Serialize, Deserialize, Debug, FromForm, Queryable)] pub struct GameNight { pub id: i32, - pub game : String, - pub datetime : String, + pub game: String, + pub datetime: String, } #[derive(Serialize, Deserialize, Debug, Validate, Clone)] pub struct Register { - #[validate(length(min = 1), custom( function = "unique_username", arg = "&'v_a diesel::SqliteConnection"))] + #[validate( + length(min = 1), + custom(function = "unique_username", arg = "&'v_a diesel::SqliteConnection") + )] pub username: String, - #[validate(email, custom( function = "unique_email", arg = "&'v_a diesel::SqliteConnection"))] + #[validate( + email, + custom(function = "unique_email", arg = "&'v_a diesel::SqliteConnection") + )] pub email: String, #[validate(length(min = 10), must_match = "password_repeat")] pub password: String, @@ -283,12 +307,12 @@ pub struct Register { #[derive(Serialize, Deserialize, Debug)] pub struct Login { pub username: String, - pub password: String + pub password: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LoginResult { pub result: bool, pub id: Option, - pub role: Option + pub role: Option, } diff --git a/backend/src/site.rs b/backend/src/site.rs index d25f1c8..a5cbaa2 100644 --- a/backend/src/site.rs +++ b/backend/src/site.rs @@ -1,88 +1,42 @@ -use std::borrow::Cow; -use serde::{Serialize, Deserialize}; -use rocket_dyn_templates::Template; -use rocket::response::{Redirect}; -use rocket::request::{FlashMessage}; -use crate::schema; +use rocket::fs::NamedFile; +use rocket::http::Method; +use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions}; +use std::io; +use std::path::{Path, PathBuf}; -#[derive(Serialize, Deserialize, Debug)] -struct FlashData { - has_data: bool, - kind: Cow<'static, str>, - message: Cow<'static, str> +pub fn make_cors() -> Cors { + let allowed_origins = AllowedOrigins::some_exact(&[ + // 4. + //CHANGE THESE TO MATCH YOUR PORTS + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://localhost:8000", + "http://0.0.0.0:8000", + ]); + CorsOptions { + // 5. + allowed_origins, + allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(), // 1. + allowed_headers: AllowedHeaders::some(&[ + "Authorization", + "Accept", + "Access-Control-Allow-Origin", // 6. + ]), + allow_credentials: true, + ..Default::default() + } + .to_cors() + .expect("error while building CORS") } -impl FlashData { - const EMPTY: Self = Self { has_data: false, message: Cow::Borrowed(""), kind: Cow::Borrowed("") }; -} - -#[derive(Serialize, Deserialize, Debug)] -struct GameNightsData { - gamenights: Vec::, - flash: FlashData -} - -#[get("/gamenights")] -pub async fn gamenights(conn: schema::DbConn) -> Template { - let gamenights = schema::get_all_gamenights(conn).await; - - let data = GameNightsData { - gamenights: gamenights, - flash: FlashData::EMPTY - }; - - Template::render("gamenights", &data) +#[get("/")] +pub async fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("../frontend/build/").join(file)) + .await + .ok() } #[get("/")] -pub async fn index() -> Redirect { - Redirect::to(uri!(gamenights)) +pub async fn index() -> io::Result { + NamedFile::open("../frontend/build/index.html").await } - -#[derive(Serialize, Deserialize, Debug)] -struct GameNightAddData { - post_url: String, - flash : FlashData -} - -#[get("/gamenight/add")] -pub async fn add_game_night(flash: Option>) -> Template { - let flash_data = match flash { - None => FlashData::EMPTY, - Some(flash) => FlashData { - has_data: true, - message: Cow::Owned(flash.message().to_string()), - kind: Cow::Owned(flash.kind().to_string()) - } - }; - - let data = GameNightAddData { - post_url: "/api/gamenight".to_string(), - flash: flash_data - }; - - Template::render("gamenight_add", &data) -} - -#[derive(Serialize, Deserialize, Debug)] -struct RegisterData { - flash : FlashData -} - -#[get("/register")] -pub async fn register(flash: Option>) -> Template { - let flash_data = match flash { - None => FlashData::EMPTY, - Some(flash) => FlashData { - has_data: true, - message: Cow::Owned(flash.message().to_string()), - kind: Cow::Owned(flash.kind().to_string()) - } - }; - - let data = RegisterData { - flash: flash_data - }; - - Template::render("register", &data) -} \ No newline at end of file