From 5a336df59fe4746720b6fa0be08accbf55a3917a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Fri, 24 Jan 2025 13:45:09 +0100 Subject: [PATCH] feat(shield-axum): use JSON for errors --- packages/core/shield/src/error.rs | 2 + packages/core/shield/src/user.rs | 5 +++ .../integrations/shield-axum/src/error.rs | 45 ++++++++++++++++++- .../shield-axum/src/routes/sign_in.rs | 6 ++- .../src/routes/sign_in_callback.rs | 6 ++- .../shield-axum/src/routes/sign_out.rs | 5 ++- .../shield-axum/src/routes/subproviders.rs | 7 ++- .../shield-axum/src/routes/user.rs | 42 ++++++++--------- 8 files changed, 87 insertions(+), 31 deletions(-) diff --git a/packages/core/shield/src/error.rs b/packages/core/shield/src/error.rs index cf56d0b..5050112 100644 --- a/packages/core/shield/src/error.rs +++ b/packages/core/shield/src/error.rs @@ -55,4 +55,6 @@ pub enum ShieldError { Request(String), #[error("{0}")] Validation(String), + #[error("Unauthorized")] + Unauthorized, } diff --git a/packages/core/shield/src/user.rs b/packages/core/shield/src/user.rs index 14b96ed..3faf65e 100644 --- a/packages/core/shield/src/user.rs +++ b/packages/core/shield/src/user.rs @@ -27,15 +27,20 @@ pub struct UpdateUser { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] pub struct EmailAddress { pub id: String, pub email: String, pub is_primary: bool, pub is_verified: bool, + #[serde(skip)] pub verification_token: Option, + #[serde(skip)] pub verification_token_expired_at: Option>, + #[serde(skip)] pub verified_at: Option>, + #[serde(skip)] pub user_id: String, } diff --git a/packages/integrations/shield-axum/src/error.rs b/packages/integrations/shield-axum/src/error.rs index 950ce0a..ff59dc1 100644 --- a/packages/integrations/shield-axum/src/error.rs +++ b/packages/integrations/shield-axum/src/error.rs @@ -1,14 +1,55 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, + Json, }; -use shield::ShieldError; +use serde::Serialize; +use shield::{ShieldError, StorageError}; + +#[derive(Serialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "utoipa", schema(as = Error))] +#[serde(rename_all = "camelCase")] +pub struct ErrorBody { + status_code: u16, + status_reason: Option, + message: String, +} + +impl ErrorBody { + fn new(status_code: StatusCode, error: ShieldError) -> Self { + Self { + status_code: status_code.as_u16(), + status_reason: status_code.canonical_reason().map(ToOwned::to_owned), + message: error.to_string(), + } + } +} pub struct RouteError(ShieldError); impl IntoResponse for RouteError { fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response() + let status_code = match &self.0 { + ShieldError::Provider(provider_error) => match provider_error { + shield::ProviderError::ProviderNotFound(_) => StatusCode::NOT_FOUND, + shield::ProviderError::SubproviderMissing => StatusCode::BAD_REQUEST, + shield::ProviderError::SubproviderNotFound(_) => StatusCode::NOT_FOUND, + }, + ShieldError::Configuration(_) => StatusCode::INTERNAL_SERVER_ERROR, + ShieldError::Session(_) => StatusCode::INTERNAL_SERVER_ERROR, + ShieldError::Storage(storage_error) => match storage_error { + StorageError::Configuration(_) => StatusCode::INTERNAL_SERVER_ERROR, + StorageError::Validation(_) => StatusCode::BAD_REQUEST, + StorageError::NotFound(_, _) => StatusCode::NOT_FOUND, + StorageError::Engine(_) => StatusCode::INTERNAL_SERVER_ERROR, + }, + ShieldError::Request(_) => StatusCode::INTERNAL_SERVER_ERROR, + ShieldError::Validation(_) => StatusCode::BAD_REQUEST, + ShieldError::Unauthorized => StatusCode::UNAUTHORIZED, + }; + + (status_code, Json(ErrorBody::new(status_code, self.0))).into_response() } } diff --git a/packages/integrations/shield-axum/src/routes/sign_in.rs b/packages/integrations/shield-axum/src/routes/sign_in.rs index a0c66b5..c58805f 100644 --- a/packages/integrations/shield-axum/src/routes/sign_in.rs +++ b/packages/integrations/shield-axum/src/routes/sign_in.rs @@ -2,7 +2,7 @@ use axum::extract::Path; use shield::{SignInRequest, User}; use crate::{ - error::RouteError, + error::{ErrorBody, RouteError}, extract::{ExtractSession, ExtractShield}, path::AuthPathParams, response::RouteResponse, @@ -21,7 +21,9 @@ use crate::{ responses( (status = 200, description = "Successfully signed in."), (status = 303, description = "Redirect to authentication provider for sign in."), - (status = 500, description = "Internal server error."), + (status = 400, description = "Bad request.", body = ErrorBody), + (status = 404, description = "Not found.", body = ErrorBody), + (status = 500, description = "Internal server error.", body = ErrorBody), ) ) )] diff --git a/packages/integrations/shield-axum/src/routes/sign_in_callback.rs b/packages/integrations/shield-axum/src/routes/sign_in_callback.rs index 7a56882..93e1da9 100644 --- a/packages/integrations/shield-axum/src/routes/sign_in_callback.rs +++ b/packages/integrations/shield-axum/src/routes/sign_in_callback.rs @@ -3,7 +3,7 @@ use serde_json::Value; use shield::{SignInCallbackRequest, User}; use crate::{ - error::RouteError, + error::{ErrorBody, RouteError}, extract::{ExtractSession, ExtractShield}, path::AuthPathParams, response::RouteResponse, @@ -21,7 +21,9 @@ use crate::{ ), responses( (status = 200, description = "Successfully signed in."), - (status = 500, description = "Internal server error."), + (status = 400, description = "Bad request.", body = ErrorBody), + (status = 404, description = "Not found.", body = ErrorBody), + (status = 500, description = "Internal server error.", body = ErrorBody), ) ) )] diff --git a/packages/integrations/shield-axum/src/routes/sign_out.rs b/packages/integrations/shield-axum/src/routes/sign_out.rs index 32167d9..8659118 100644 --- a/packages/integrations/shield-axum/src/routes/sign_out.rs +++ b/packages/integrations/shield-axum/src/routes/sign_out.rs @@ -1,7 +1,7 @@ use shield::User; use crate::{ - error::RouteError, + error::{ErrorBody, RouteError}, extract::{ExtractSession, ExtractShield}, response::RouteResponse, }; @@ -15,7 +15,8 @@ use crate::{ description = "Sign out of the current account.", responses( (status = 201, description = "Successfully signed out."), - (status = 500, description = "Internal server error.") + (status = 400, description = "Bad request.", body = ErrorBody), + (status = 500, description = "Internal server error.", body = ErrorBody), ) ) )] diff --git a/packages/integrations/shield-axum/src/routes/subproviders.rs b/packages/integrations/shield-axum/src/routes/subproviders.rs index e420c25..8b497ee 100644 --- a/packages/integrations/shield-axum/src/routes/subproviders.rs +++ b/packages/integrations/shield-axum/src/routes/subproviders.rs @@ -1,7 +1,10 @@ use axum::Json; use shield::{SubproviderVisualisation, User}; -use crate::{error::RouteError, extract::ExtractShield}; +use crate::{ + error::{ErrorBody, RouteError}, + extract::ExtractShield, +}; #[cfg_attr( feature = "utoipa", @@ -12,7 +15,7 @@ use crate::{error::RouteError, extract::ExtractShield}; description = "Get a list of authentication subproviders.", responses( (status = 200, description = "List of authentication subproviders.", body = Vec), - (status = 500, description = "Internal server error.") + (status = 500, description = "Internal server error.", body = ErrorBody), ) ) )] diff --git a/packages/integrations/shield-axum/src/routes/user.rs b/packages/integrations/shield-axum/src/routes/user.rs index eb6b7f5..336751d 100644 --- a/packages/integrations/shield-axum/src/routes/user.rs +++ b/packages/integrations/shield-axum/src/routes/user.rs @@ -1,31 +1,31 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; +use axum::Json; use serde::{Deserialize, Serialize}; -use shield::User; +use shield::{EmailAddress, ShieldError, User}; -use crate::extract::ExtractUser; +use crate::{ + error::{ErrorBody, RouteError}, + extract::ExtractUser, +}; #[derive(Deserialize, Serialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "utoipa", schema(as = User))] #[serde(rename_all = "camelCase")] -struct UserBody { +pub struct UserBody { id: String, name: Option, + email_addresses: Vec, } impl UserBody { - async fn new(user: U) -> Self { - // TODO: Include email addresses. - // let email_addresses = user.email_addresses().await; + async fn new(user: U) -> Result { + let email_addresses = user.email_addresses().await?; - Self { + Ok(Self { id: user.id(), name: user.name(), - } + email_addresses, + }) } } @@ -38,15 +38,15 @@ impl UserBody { description = "Get the current user account.", responses( (status = 200, description = "Current user account.", body = UserBody), - (status = 401, description = "No account signed in."), - (status = 500, description = "Internal server error."), + (status = 401, description = "No account signed in.", body = ErrorBody), + (status = 500, description = "Internal server error.", body = ErrorBody), ) ) )] -pub async fn user(ExtractUser(user): ExtractUser) -> Response { - // TODO: Send JSON error using some util. - match user { - Some(user) => Json(UserBody::new(user).await).into_response(), - None => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), - } +pub async fn user( + ExtractUser(user): ExtractUser, +) -> Result, RouteError> { + let user = user.ok_or(ShieldError::Unauthorized)?; + + Ok(Json(UserBody::new(user).await?)) }