use uuid::Uuid; 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 jsonwebtoken::{EncodingKey, Header}; use rocket::http::Status; use rocket::request::Outcome; use rocket::request::{FromRequest, Request}; use rocket::serde::json::{json, Json, Value}; use rocket::State; use serde::{Deserialize, Serialize}; use std::borrow::Cow; 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)] struct ApiResponse { result: Cow<'static, str>, #[serde(skip_serializing_if = "Option::is_none")] message: Option>, #[serde(skip_serializing_if = "Option::is_none")] user: Option, #[serde(skip_serializing_if = "Option::is_none")] gamenights: Option>, #[serde(skip_serializing_if = "Option::is_none")] games: Option>, } impl ApiResponse { const SUCCES_RESULT: Cow<'static, str> = Cow::Borrowed("Ok"); const FAILURE_RESULT: Cow<'static, str> = Cow::Borrowed("Failure"); const SUCCES: Self = Self { result: Self::SUCCES_RESULT, message: None, user: None, gamenights: None, games: None, }; fn error(message: String) -> Self { Self { result: Self::FAILURE_RESULT, message: Some(Cow::Owned(message)), user: None, gamenights: None, games: None, } } fn login_response(user: schema::User, jwt: String) -> Self { Self { result: Self::SUCCES_RESULT, message: None, user: Some(UserWithToken { user: user, jwt: jwt }), gamenights: None, games: None, } } fn gamenight_response(gamenights: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, user: None, gamenights: Some(gamenights), games: None, } } fn games_response(games: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, user: None, gamenights: None, games: Some(games), } } } #[derive(Debug)] pub enum ApiError { RequestError(String), } const AUTH_HEADER: &str = "Authorization"; const BEARER: &str = "Bearer "; #[rocket::async_trait] impl<'r> FromRequest<'r> for schema::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(()) } }; if !header.starts_with(BEARER) { return Outcome::Forward(()); }; 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(), ) { Ok(token) => token, Err(_) => { return Outcome::Forward(()) } }; let id = token.claims.uid; let conn = req.guard::().await.unwrap(); return match schema::get_user(conn, id).await { Ok(o) => Outcome::Success(o), Err(_) => Outcome::Forward(()) } } } #[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()))) } } #[get("/gamenights", rank = 2)] pub async fn gamenights_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GameNightInput { pub name: String, pub datetime: String, pub owner_id: Option, pub game_list: Vec, } impl Into for GameNightInput { fn into(self) -> schema::GameNight { schema::GameNight { id: Uuid::new_v4(), name: self.name, datetime: self.datetime, owner_id: self.owner_id.unwrap() } } } #[post("/gamenights", format = "application/json", data = "")] pub async fn gamenights_post_json( conn: DbConn, user: schema::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 { Ok(_) => (), Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))) }; match schema::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()))) } } #[post("/gamenights", rank = 2)] pub async fn gamenights_post_json_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[delete("/gamenights", format = "application/json", data = "")] pub async fn gamenights_delete_json( conn: DbConn, user: schema::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()))) } return ApiResponseVariant::Value(json!(ApiResponse::SUCCES)) } match schema::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()))) } return ApiResponseVariant::Value(json!(ApiResponse::SUCCES)) } }, 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 { let register = register_json.into_inner(); let register_clone = register.clone(); match conn .run(move |c| register_clone.validate_args((c, c))) .await { Ok(()) => (), 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()))), } } #[derive(Debug, Serialize, Deserialize)] struct Claims { exp: i64, uid: Uuid, role: schema::Role, } #[post("/login", format = "application/json", data = "")] 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 user = login_result.user.unwrap(); let my_claims = Claims { exp: Utc::now().timestamp() + chrono::Duration::days(7).num_seconds(), uid: user.id, role: user.role, }; let secret = &config.inner().jwt_secret; match encode( &Header::default(), &my_claims, &EncodingKey::from_secret(secret.as_bytes()), ) { Ok(token) => ApiResponseVariant::Value(json!(ApiResponse::login_response(user, token))), Err(error) => { ApiResponseVariant::Value(json!(ApiResponse::error(error.to_string()))) } } } } } #[get("/games")] pub async fn games(conn: DbConn, _user: schema::User) -> ApiResponseVariant { match schema::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()))) } } #[get("/games", rank = 2)] pub async fn games_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) }