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>, } 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, }; fn error(message: String) -> Self { Self { result: Self::FAILURE_RESULT, message: Some(Cow::Owned(message)), user: None, gamenights: 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, } } fn gamenight_response(gamenights: Vec) -> Self { Self { result: Self::SUCCES_RESULT, message: None, user: None, gamenights: Some(gamenights), } } } #[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 Outcome::Success(schema::get_user(conn, id).await); } } #[get("/gamenights")] pub async fn gamenights(conn: DbConn, _user: schema::User) -> ApiResponseVariant { let gamenights = schema::get_all_gamenights(conn).await; ApiResponseVariant::Value(json!(ApiResponse::gamenight_response(gamenights))) } #[get("/gamenights", rank = 2)] pub async fn gamenights_unauthorized() -> ApiResponseVariant { ApiResponseVariant::Status(Status::Unauthorized) } #[post("/gamenight", format = "application/json", data = "")] pub async fn gamenight_post_json( conn: DbConn, _user: schema::User, gamenight_json: Json, ) -> ApiResponseVariant { match schema::insert_gamenight(conn, gamenight_json.into_inner()).await { Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)), Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))), } } #[post("/gamenight", rank = 2)] pub async fn gamenight_post_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: i32, 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()))) } } } } }