From dc8cb834e09849da18e3f42856d09cd388b09c06 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 2 Jan 2025 22:34:10 -0500 Subject: [PATCH] Handle all applications errors in one location --- Cargo.lock | 12 +++ visions/server/Cargo.toml | 2 +- .../server/src/handlers/game_management.rs | 29 +++-- visions/server/src/handlers/mod.rs | 32 +++++- .../server/src/handlers/user_management.rs | 82 +++++--------- visions/server/src/routes.rs | 100 +++++++++--------- 6 files changed, 133 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 532f500..f4cba53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,6 +314,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http 1.2.0", @@ -361,6 +362,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "axum-test" version = "16.4.1" diff --git a/visions/server/Cargo.toml b/visions/server/Cargo.toml index 1e9f78d..6d1cf60 100644 --- a/visions/server/Cargo.toml +++ b/visions/server/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" async-std = { version = "1.13.0" } async-trait = { version = "0.1.83" } authdb = { path = "../../authdb/" } -axum = { version = "0.7.9" } +axum = { version = "0.7.9", features = [ "macros" ] } futures = { version = "0.3.31" } include_dir = { version = "0.7.4" } lazy_static = { version = "1.5.0" } diff --git a/visions/server/src/handlers/game_management.rs b/visions/server/src/handlers/game_management.rs index 818bb93..9e72bc7 100644 --- a/visions/server/src/handlers/game_management.rs +++ b/visions/server/src/handlers/game_management.rs @@ -1,13 +1,18 @@ -use axum::{http::{HeaderMap, StatusCode}, Json}; +use axum::{ + http::{HeaderMap, StatusCode}, + Json, +}; use result_extended::ResultExt; use serde::{Deserialize, Serialize}; -use crate::{database::GameId, core::Core}; +use crate::{ + core::Core, + database::GameId, + types::{AppError, FatalError}, +}; use super::auth_required; - - #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct CreateGameRequest { pub game_type: String, @@ -18,16 +23,10 @@ pub async fn create_game( core: Core, headers: HeaderMap, req: CreateGameRequest, -) -> (StatusCode, Json>) { - println!("create game handler"); +) -> ResultExt { auth_required(core.clone(), headers, |user| async move { - let game = core.create_game(&user.id, &req.game_type, &req.game_name).await; - println!("create_game completed: {:?}", game); - match game { - ResultExt::Ok(game_id) => (StatusCode::OK, Json(Some(game_id))), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(fatal) => panic!("{}", fatal), - } - }).await + core.create_game(&user.id, &req.game_type, &req.game_name) + .await + }) + .await } - diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index d2d7056..0e1ad18 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -1,18 +1,48 @@ mod game_management; mod user_management; +use axum::{http::StatusCode, Json}; +use futures::Future; pub use game_management::*; +use typeshare::typeshare; pub use user_management::*; use result_extended::ResultExt; use serde::{Deserialize, Serialize}; -use crate::core::Core; +use crate::{ + core::Core, + types::{AppError, FatalError}, +}; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct HealthCheck { pub ok: bool, } +pub async fn wrap_handler(f: F) -> (StatusCode, Json>) +where + F: FnOnce() -> Fut, + Fut: Future>, +{ + match f().await { + ResultExt::Ok(val) => (StatusCode::OK, Json(Some(val))), + ResultExt::Err(AppError::BadRequest) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Err(AppError::CouldNotCreateObject) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Err(AppError::NotFound(_)) => (StatusCode::NOT_FOUND, Json(None)), + ResultExt::Err(AppError::Inaccessible(_)) => (StatusCode::NOT_FOUND, Json(None)), + ResultExt::Err(AppError::PermissionDenied) => (StatusCode::FORBIDDEN, Json(None)), + ResultExt::Err(AppError::AuthFailed) => (StatusCode::UNAUTHORIZED, Json(None)), + ResultExt::Err(AppError::JsonError(_)) => (StatusCode::INTERNAL_SERVER_ERROR, Json(None)), + ResultExt::Err(AppError::UnexpectedError(_)) => { + (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) + } + ResultExt::Err(AppError::UsernameUnavailable) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(err) => { + panic!("The server encountered a fatal error: {}", err); + } + } +} + pub async fn healthcheck(core: Core) -> Vec { match core.status().await { ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck { diff --git a/visions/server/src/handlers/user_management.rs b/visions/server/src/handlers/user_management.rs index 3a30cfe..dcbad23 100644 --- a/visions/server/src/handlers/user_management.rs +++ b/visions/server/src/handlers/user_management.rs @@ -3,7 +3,7 @@ use axum::{ Json, }; use futures::Future; -use result_extended::{error, ok, ResultExt}; +use result_extended::{error, ok, return_error, ResultExt}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; @@ -58,16 +58,14 @@ pub async fn auth_required( core: Core, headers: HeaderMap, f: F, -) -> (StatusCode, Json>) +) -> ResultExt where F: FnOnce(User) -> Fut, - Fut: Future>)>, + Fut: Future>, { - match check_session(&core, headers).await { - ResultExt::Ok(Some(user)) => f(user).await, - ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), + match return_error!(check_session(&core, headers).await) { + Some(user) => f(user).await, + None => error(AppError::AuthFailed), } } @@ -75,94 +73,64 @@ pub async fn admin_required( core: Core, headers: HeaderMap, f: F, -) -> (StatusCode, Json>) +) -> ResultExt where F: FnOnce(User) -> Fut, - Fut: Future>)>, + Fut: Future>, { - match check_session(&core, headers).await { - ResultExt::Ok(Some(user)) => { + match return_error!(check_session(&core, headers).await) { + Some(user) => { if user.admin { f(user).await } else { - (StatusCode::FORBIDDEN, Json(None)) + error(AppError::PermissionDenied) } } - ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), + None => error(AppError::AuthFailed), } } pub async fn check_password( core: Core, req: Json, -) -> (StatusCode, Json>) { +) -> ResultExt { let Json(AuthRequest { username, password }) = req; - match core.auth(&username, &password).await { - ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))), - ResultExt::Err(_err) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Fatal(err) => panic!("Fatal: {}", err), - } + core.auth(&username, &password).await } pub async fn get_user( core: Core, headers: HeaderMap, user_id: Option, -) -> (StatusCode, Json>) { +) -> ResultExt, AppError, FatalError> { auth_required(core.clone(), headers, |user| async move { match user_id { - Some(user_id) => match core.user(user_id).await { - ResultExt::Ok(Some(user)) => (StatusCode::OK, Json(Some(user))), - ResultExt::Ok(None) => (StatusCode::NOT_FOUND, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), - }, - None => ( - StatusCode::OK, - Json(Some(UserProfile { - id: UserId::from(user.id), - name: user.name, - is_admin: user.admin, - games: vec![], - })), - ), + Some(user_id) => core.user(user_id).await, + None => core.user(user.id).await, } - }) - .await + }).await } pub async fn create_user( core: Core, headers: HeaderMap, req: CreateUserRequest, -) -> (StatusCode, Json>) { +) -> ResultExt { admin_required(core.clone(), headers, |_admin| async { - match core.create_user(&req.username).await { - ResultExt::Ok(_) => (StatusCode::OK, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(fatal) => panic!("{}", fatal), - } - }) - .await + core.create_user(&req.username).await + }).await } pub async fn set_password( core: Core, headers: HeaderMap, req: SetPasswordRequest, -) -> (StatusCode, Json>) { +) -> ResultExt<(), AppError, FatalError> { auth_required(core.clone(), headers, |user| async { if req.password_1 == req.password_2 { - match core.set_password(user.id, req.password_1).await { - ResultExt::Ok(_) => (StatusCode::OK, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(fatal) => panic!("{}", fatal), - } + core.set_password(user.id, req.password_1).await } else { - (StatusCode::BAD_REQUEST, Json(None)) + error(AppError::BadRequest) } - }) - .await + }).await } diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index 76150ef..94369bd 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -1,16 +1,16 @@ use axum::{ extract::Path, - http::HeaderMap, + http::{HeaderMap, StatusCode}, routing::{get, post, put}, Json, Router, }; use crate::{ core::Core, - database::UserId, + database::{SessionId, UserId}, handlers::{ - check_password, create_game, create_user, get_user, healthcheck, set_password, AuthRequest, - CreateGameRequest, CreateUserRequest, SetPasswordRequest, + check_password, create_game, create_user, get_user, healthcheck, set_password, + wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest, }, }; @@ -27,54 +27,54 @@ pub fn routes(core: Core) -> Router { "/api/v1/auth", post({ let core = core.clone(); - move |req: Json| check_password(core, req) - }), - ) - .route( - // By default, just get the self user. - "/api/v1/user", - get({ - let core = core.clone(); - move |headers: HeaderMap| get_user(core, headers, None) - }) - .put({ - let core = core.clone(); - move |headers: HeaderMap, req: Json| { - let Json(req) = req; - create_user(core, headers, req) - } - }), - ) - .route( - "/api/v1/user/password", - put({ - let core = core.clone(); - move |headers: HeaderMap, req: Json| { - let Json(req) = req; - set_password(core, headers, req) - } - }), - ) - .route( - "/api/v1/user/:user_id", - get({ - let core = core.clone(); - move |user_id: Path, headers: HeaderMap| { - let Path(user_id) = user_id; - get_user(core, headers, Some(user_id)) - } - }), - ) - .route( - "/api/v1/games", - put({ - let core = core.clone(); - move |headers: HeaderMap, req: Json| { - let Json(req) = req; - create_game(core, headers, req) - } + move |req: Json| wrap_handler(|| check_password(core, req)) }), ) + .route( + // By default, just get the self user. + "/api/v1/user", + get({ + let core = core.clone(); + move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None)) + }) + .put({ + let core = core.clone(); + move |headers: HeaderMap, req: Json| { + let Json(req) = req; + wrap_handler(|| create_user(core, headers, req)) + } + }), + ) + .route( + "/api/v1/user/password", + put({ + let core = core.clone(); + move |headers: HeaderMap, req: Json| { + let Json(req) = req; + wrap_handler(|| set_password(core, headers, req)) + } + }), + ) + .route( + "/api/v1/user/:user_id", + get({ + let core = core.clone(); + move |user_id: Path, headers: HeaderMap| { + let Path(user_id) = user_id; + wrap_handler(|| get_user(core, headers, Some(user_id))) + } + }), + ) + .route( + "/api/v1/games", + put({ + let core = core.clone(); + move |headers: HeaderMap, req: Json| { + let Json(req) = req; + wrap_handler(|| create_game(core, headers, req)) + } + }), + ) } #[cfg(test)]