Handle all applications errors in one location

This commit is contained in:
Savanni D'Gerinel 2025-01-02 22:34:10 -05:00
parent 4dc7a50000
commit dc8cb834e0
6 changed files with 133 additions and 124 deletions

12
Cargo.lock generated
View File

@ -314,6 +314,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"axum-macros",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.2.0", "http 1.2.0",
@ -361,6 +362,17 @@ dependencies = [
"tracing", "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]] [[package]]
name = "axum-test" name = "axum-test"
version = "16.4.1" version = "16.4.1"

View File

@ -9,7 +9,7 @@ edition = "2021"
async-std = { version = "1.13.0" } async-std = { version = "1.13.0" }
async-trait = { version = "0.1.83" } async-trait = { version = "0.1.83" }
authdb = { path = "../../authdb/" } authdb = { path = "../../authdb/" }
axum = { version = "0.7.9" } axum = { version = "0.7.9", features = [ "macros" ] }
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
include_dir = { version = "0.7.4" } include_dir = { version = "0.7.4" }
lazy_static = { version = "1.5.0" } lazy_static = { version = "1.5.0" }

View File

@ -1,13 +1,18 @@
use axum::{http::{HeaderMap, StatusCode}, Json}; use axum::{
http::{HeaderMap, StatusCode},
Json,
};
use result_extended::ResultExt; use result_extended::ResultExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{database::GameId, core::Core}; use crate::{
core::Core,
database::GameId,
types::{AppError, FatalError},
};
use super::auth_required; use super::auth_required;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct CreateGameRequest { pub struct CreateGameRequest {
pub game_type: String, pub game_type: String,
@ -18,16 +23,10 @@ pub async fn create_game(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
req: CreateGameRequest, req: CreateGameRequest,
) -> (StatusCode, Json<Option<GameId>>) { ) -> ResultExt<GameId, AppError, FatalError> {
println!("create game handler");
auth_required(core.clone(), headers, |user| async move { auth_required(core.clone(), headers, |user| async move {
let game = core.create_game(&user.id, &req.game_type, &req.game_name).await; core.create_game(&user.id, &req.game_type, &req.game_name)
println!("create_game completed: {:?}", game); .await
match game { })
ResultExt::Ok(game_id) => (StatusCode::OK, Json(Some(game_id))), .await
ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(fatal) => panic!("{}", fatal),
} }
}).await
}

View File

@ -1,18 +1,48 @@
mod game_management; mod game_management;
mod user_management; mod user_management;
use axum::{http::StatusCode, Json};
use futures::Future;
pub use game_management::*; pub use game_management::*;
use typeshare::typeshare;
pub use user_management::*; pub use user_management::*;
use result_extended::ResultExt; use result_extended::ResultExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::core::Core; use crate::{
core::Core,
types::{AppError, FatalError},
};
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct HealthCheck { pub struct HealthCheck {
pub ok: bool, pub ok: bool,
} }
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
{
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<u8> { pub async fn healthcheck(core: Core) -> Vec<u8> {
match core.status().await { match core.status().await {
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck { ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {

View File

@ -3,7 +3,7 @@ use axum::{
Json, Json,
}; };
use futures::Future; use futures::Future;
use result_extended::{error, ok, ResultExt}; use result_extended::{error, ok, return_error, ResultExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
@ -58,16 +58,14 @@ pub async fn auth_required<F, A, Fut>(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
f: F, f: F,
) -> (StatusCode, Json<Option<A>>) ) -> ResultExt<A, AppError, FatalError>
where where
F: FnOnce(User) -> Fut, F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>, Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
{ {
match check_session(&core, headers).await { match return_error!(check_session(&core, headers).await) {
ResultExt::Ok(Some(user)) => f(user).await, Some(user) => f(user).await,
ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), None => error(AppError::AuthFailed),
ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(err) => panic!("{}", err),
} }
} }
@ -75,94 +73,64 @@ pub async fn admin_required<F, A, Fut>(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
f: F, f: F,
) -> (StatusCode, Json<Option<A>>) ) -> ResultExt<A, AppError, FatalError>
where where
F: FnOnce(User) -> Fut, F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>, Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
{ {
match check_session(&core, headers).await { match return_error!(check_session(&core, headers).await) {
ResultExt::Ok(Some(user)) => { Some(user) => {
if user.admin { if user.admin {
f(user).await f(user).await
} else { } else {
(StatusCode::FORBIDDEN, Json(None)) error(AppError::PermissionDenied)
} }
} }
ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), None => error(AppError::AuthFailed),
ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(err) => panic!("{}", err),
} }
} }
pub async fn check_password( pub async fn check_password(
core: Core, core: Core,
req: Json<AuthRequest>, req: Json<AuthRequest>,
) -> (StatusCode, Json<Option<SessionId>>) { ) -> ResultExt<SessionId, AppError, FatalError> {
let Json(AuthRequest { username, password }) = req; let Json(AuthRequest { username, password }) = req;
match core.auth(&username, &password).await { 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),
}
} }
pub async fn get_user( pub async fn get_user(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
user_id: Option<UserId>, user_id: Option<UserId>,
) -> (StatusCode, Json<Option<UserProfile>>) { ) -> ResultExt<Option<UserProfile>, AppError, FatalError> {
auth_required(core.clone(), headers, |user| async move { auth_required(core.clone(), headers, |user| async move {
match user_id { match user_id {
Some(user_id) => match core.user(user_id).await { Some(user_id) => core.user(user_id).await,
ResultExt::Ok(Some(user)) => (StatusCode::OK, Json(Some(user))), None => core.user(user.id).await,
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![],
})),
),
} }
}) }).await
.await
} }
pub async fn create_user( pub async fn create_user(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
req: CreateUserRequest, req: CreateUserRequest,
) -> (StatusCode, Json<Option<()>>) { ) -> ResultExt<UserId, AppError, FatalError> {
admin_required(core.clone(), headers, |_admin| async { admin_required(core.clone(), headers, |_admin| async {
match core.create_user(&req.username).await { core.create_user(&req.username).await
ResultExt::Ok(_) => (StatusCode::OK, Json(None)), }).await
ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(fatal) => panic!("{}", fatal),
}
})
.await
} }
pub async fn set_password( pub async fn set_password(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
req: SetPasswordRequest, req: SetPasswordRequest,
) -> (StatusCode, Json<Option<()>>) { ) -> ResultExt<(), AppError, FatalError> {
auth_required(core.clone(), headers, |user| async { auth_required(core.clone(), headers, |user| async {
if req.password_1 == req.password_2 { if req.password_1 == req.password_2 {
match core.set_password(user.id, req.password_1).await { 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),
}
} else { } else {
(StatusCode::BAD_REQUEST, Json(None)) error(AppError::BadRequest)
} }
}) }).await
.await
} }

View File

@ -1,16 +1,16 @@
use axum::{ use axum::{
extract::Path, extract::Path,
http::HeaderMap, http::{HeaderMap, StatusCode},
routing::{get, post, put}, routing::{get, post, put},
Json, Router, Json, Router,
}; };
use crate::{ use crate::{
core::Core, core::Core,
database::UserId, database::{SessionId, UserId},
handlers::{ handlers::{
check_password, create_game, create_user, get_user, healthcheck, set_password, AuthRequest, check_password, create_game, create_user, get_user, healthcheck, set_password,
CreateGameRequest, CreateUserRequest, SetPasswordRequest, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
}, },
}; };
@ -27,7 +27,7 @@ pub fn routes(core: Core) -> Router {
"/api/v1/auth", "/api/v1/auth",
post({ post({
let core = core.clone(); let core = core.clone();
move |req: Json<AuthRequest>| check_password(core, req) move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
}), }),
) )
.route( .route(
@ -35,13 +35,13 @@ pub fn routes(core: Core) -> Router {
"/api/v1/user", "/api/v1/user",
get({ get({
let core = core.clone(); let core = core.clone();
move |headers: HeaderMap| get_user(core, headers, None) move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None))
}) })
.put({ .put({
let core = core.clone(); let core = core.clone();
move |headers: HeaderMap, req: Json<CreateUserRequest>| { move |headers: HeaderMap, req: Json<CreateUserRequest>| {
let Json(req) = req; let Json(req) = req;
create_user(core, headers, req) wrap_handler(|| create_user(core, headers, req))
} }
}), }),
) )
@ -51,7 +51,7 @@ pub fn routes(core: Core) -> Router {
let core = core.clone(); let core = core.clone();
move |headers: HeaderMap, req: Json<SetPasswordRequest>| { move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
let Json(req) = req; let Json(req) = req;
set_password(core, headers, req) wrap_handler(|| set_password(core, headers, req))
} }
}), }),
) )
@ -61,7 +61,7 @@ pub fn routes(core: Core) -> Router {
let core = core.clone(); let core = core.clone();
move |user_id: Path<UserId>, headers: HeaderMap| { move |user_id: Path<UserId>, headers: HeaderMap| {
let Path(user_id) = user_id; let Path(user_id) = user_id;
get_user(core, headers, Some(user_id)) wrap_handler(|| get_user(core, headers, Some(user_id)))
} }
}), }),
) )
@ -71,7 +71,7 @@ pub fn routes(core: Core) -> Router {
let core = core.clone(); let core = core.clone();
move |headers: HeaderMap, req: Json<CreateGameRequest>| { move |headers: HeaderMap, req: Json<CreateGameRequest>| {
let Json(req) = req; let Json(req) = req;
create_game(core, headers, req) wrap_handler(|| create_game(core, headers, req))
} }
}), }),
) )