use crate::schema; use crate::schema::admin::RegistrationToken; use crate::schema::gamenight::*; use crate::schema::users::*; use crate::schema::DatabaseError; use crate::schema::DbConn; use crate::AppConfig; use chrono::DateTime; use chrono::Utc; use futures::future::join_all; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use rocket::http::Status; 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), Value(Value), } #[derive(Serialize, Deserialize, Debug)] pub enum ApiData { #[serde(rename = "user")] User(UserWithToken), #[serde(rename = "gamenights")] Gamenights(Vec), #[serde(rename = "gamenight")] Gamenight(GamenightOutput), #[serde(rename = "games")] Games(Vec), #[serde(rename = "registration_tokens")] RegistrationTokens(Vec), } #[derive(Serialize, Deserialize, Debug)] struct ApiResponse { result: Cow<'static, str>, #[serde(skip_serializing_if = "Option::is_none")] message: Option>, #[serde(flatten, skip_serializing_if = "Option::is_none")] data: 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, data: None, }; fn error(message: String) -> Self { Self { result: Self::FAILURE_RESULT, message: Some(Cow::Owned(message)), data: None, } } fn login_response(user: User, jwt: String) -> Self { Self { result: Self::SUCCES_RESULT, message: None, data: Some(ApiData::User(UserWithToken { user: user, jwt: jwt, })), } } fn gamenights_response(gamenights: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, data: Some(ApiData::Gamenights(gamenights)), } } fn gamenight_response(gamenight: GamenightOutput) -> Self { Self { result: Self::SUCCES_RESULT, message: None, data: Some(ApiData::Gamenight(gamenight)), } } fn games_response(games: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, data: Some(ApiData::Games(games)), } } fn registration_tokens_response(tokens: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, data: Some(ApiData::RegistrationTokens(tokens)), } } } #[derive(Debug)] pub enum ApiError { RequestError(String), } const AUTH_HEADER: &str = "Authorization"; const BEARER: &str = "Bearer "; #[rocket::async_trait] 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(()), }; 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 get_user(conn, id).await { Ok(o) => Outcome::Success(o), Err(_) => Outcome::Forward(()), }; } } #[derive(Debug, Serialize, Deserialize)] pub struct GamenightOutput { #[serde(flatten)] gamenight: Gamenight, game_list: Vec, participants: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct GamenightUpdate { action: String, } #[patch( "/gamenights/", format = "application/json", data = "" )] pub async fn patch_gamenight( conn: DbConn, user: User, gamenight_id: String, patch_json: Json, ) -> ApiResponseVariant { let uuid = Uuid::parse_str(&gamenight_id).unwrap(); let patch = patch_json.into_inner(); match patch.action.as_str() { "RemoveParticipant" => { let entry = GamenightParticipantsEntry { gamenight_id: uuid, user_id: user.id, }; match remove_participant(&conn, entry).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } "AddParticipant" => { let entry = GamenightParticipantsEntry { gamenight_id: uuid, user_id: user.id, }; match add_participant(&conn, entry).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } _ => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), } } #[get("/gamenights/")] pub async fn gamenight(conn: DbConn, _user: User, gamenight_id: String) -> ApiResponseVariant { let uuid = Uuid::parse_str(&gamenight_id).unwrap(); let gamenight = match get_gamenight(&conn, uuid).await { Ok(result) => result, Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), }; let games = match get_games_of_gamenight(&conn, uuid).await { Ok(result) => result, Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), }; let participants = match load_participants(&conn, uuid).await { Ok(result) => result, Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), }; let gamenight_output = GamenightOutput { gamenight: gamenight, game_list: games, participants: participants, }; return ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(gamenight_output))); } #[get("/gamenights")] 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::gamenights_response(result))), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.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: DateTime, pub owner_id: Option, pub game_list: Vec, } 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(), } } } #[post("/gamenights", format = "application/json", data = "")] pub async fn gamenights_post_json( conn: DbConn, 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 add_unknown_games(&conn, &mut mutable_game_list).await { Ok(_) => (), Err(err) => return ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), }; 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 add_participant(&conn, participant).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: User, delete_gamenight_json: Json, ) -> ApiResponseVariant { 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 get_gamenight(&conn, delete_gamenight_json.game_id).await { Ok(gamenight) => { if user.id == gamenight.owner_id { 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)); } } 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 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: Role, } #[post("/login", format = "application/json", data = "")] pub async fn login_post_json( conn: DbConn, config: &State, login_json: Json, ) -> ApiResponseVariant { 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 { 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: 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()))), } } #[get("/games", rank = 2)] 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 add_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) } #[derive(Deserialize)] pub struct RegistrationTokenData { single_use: bool, expires: Option>, } impl Into for RegistrationTokenData { fn into(self) -> RegistrationToken { use rand::Rng; let random_bytes = rand::thread_rng().gen::<[u8; 24]>(); RegistrationToken { id: Uuid::new_v4(), token: base64::encode_config(random_bytes, base64::URL_SAFE), single_use: self.single_use, expires: self.expires, } } } #[post( "/admin/registration_tokens", format = "application/json", data = "" )] pub async fn add_registration_token( conn: DbConn, user: User, token_json: Json, ) -> ApiResponseVariant { if user.role != Role::Admin { return ApiResponseVariant::Status(Status::Unauthorized); } match schema::admin::add_registration_token(&conn, token_json.into_inner().into()).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } #[post("/admin/registration_tokens", rank = 2)] pub async fn add_registration_token_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[get("/admin/registration_tokens")] pub async fn get_registration_tokens(conn: DbConn, user: User) -> ApiResponseVariant { if user.role != Role::Admin { return ApiResponseVariant::Status(Status::Unauthorized); } match schema::admin::get_all_registration_tokens(&conn).await { Ok(results) => { ApiResponseVariant::Value(json!(ApiResponse::registration_tokens_response(results))) } Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } #[get("/admin/registration_tokens", rank = 2)] pub async fn get_registration_tokens_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[delete("/admin/registration_tokens/")] pub async fn delete_registration_tokens( conn: DbConn, user: User, gamenight_id: String, ) -> ApiResponseVariant { if user.role != Role::Admin { return ApiResponseVariant::Status(Status::Unauthorized); } let uuid = Uuid::parse_str(&gamenight_id).unwrap(); match schema::admin::delete_registration_token(&conn, uuid).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } #[delete("/admin/registration_tokens", rank = 2)] pub async fn delete_registration_tokens_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) }