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 = [
"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"

View File

@ -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" }

View File

@ -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<Option<GameId>>) {
println!("create game handler");
) -> ResultExt<GameId, AppError, FatalError> {
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
}

View File

@ -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, 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> {
match core.status().await {
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {

View File

@ -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<F, A, Fut>(
core: Core,
headers: HeaderMap,
f: F,
) -> (StatusCode, Json<Option<A>>)
) -> ResultExt<A, AppError, FatalError>
where
F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
{
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<F, A, Fut>(
core: Core,
headers: HeaderMap,
f: F,
) -> (StatusCode, Json<Option<A>>)
) -> ResultExt<A, AppError, FatalError>
where
F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
{
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<AuthRequest>,
) -> (StatusCode, Json<Option<SessionId>>) {
) -> ResultExt<SessionId, AppError, FatalError> {
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<UserId>,
) -> (StatusCode, Json<Option<UserProfile>>) {
) -> ResultExt<Option<UserProfile>, 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<Option<()>>) {
) -> ResultExt<UserId, AppError, FatalError> {
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<Option<()>>) {
) -> 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
}

View File

@ -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<AuthRequest>| 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<CreateUserRequest>| {
let Json(req) = req;
create_user(core, headers, req)
}
}),
)
.route(
"/api/v1/user/password",
put({
let core = core.clone();
move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
let Json(req) = req;
set_password(core, headers, req)
}
}),
)
.route(
"/api/v1/user/:user_id",
get({
let core = core.clone();
move |user_id: Path<UserId>, 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<CreateGameRequest>| {
let Json(req) = req;
create_game(core, headers, req)
}
move |req: Json<AuthRequest>| 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<CreateUserRequest>| {
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<SetPasswordRequest>| {
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<UserId>, 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<CreateGameRequest>| {
let Json(req) = req;
wrap_handler(|| create_game(core, headers, req))
}
}),
)
}
#[cfg(test)]