Change requests to return an AppError on failure
This commit is contained in:
parent
8e222ff2e9
commit
8d4a987f0d
9 changed files with 109 additions and 47 deletions
|
@ -1,6 +1,10 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::AppError;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiResponse<T> {
|
||||
|
@ -12,7 +16,7 @@ pub struct ApiResponse<T> {
|
|||
pub data: Option<T>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<&'static str>,
|
||||
pub error: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
|
@ -24,14 +28,6 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn _new_empty() -> ApiResponse<T> {
|
||||
Self {
|
||||
meta: None,
|
||||
data: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _new_with_metadata(data: T, meta: ApiResponseMetadata) -> ApiResponse<T> {
|
||||
Self {
|
||||
meta: Some(meta),
|
||||
|
@ -46,11 +42,27 @@ impl<T> ApiResponse<T> {
|
|||
}
|
||||
|
||||
impl ApiResponse<()> {
|
||||
pub fn _new_empty() -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
data: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(error: &'static str) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
data: None,
|
||||
error: Some(error),
|
||||
error: Some(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_error(error: AppError) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
data: None,
|
||||
error: Some(error.to_string().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ use std::{borrow::Cow, error::Error, fmt::Display, io};
|
|||
|
||||
use axum::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use sqlx::{migrate::MigrateError, Error as SqlxError};
|
||||
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
|
||||
|
||||
use super::ApiResponse;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
|
@ -18,8 +20,8 @@ impl AppError {
|
|||
Self::new(ErrorKind::AppStartupError(error))
|
||||
}
|
||||
|
||||
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
||||
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
||||
pub fn duplicate_record(message: &str) -> Self {
|
||||
Self::new(ErrorKind::DuplicateRecord(message.to_owned()))
|
||||
}
|
||||
|
||||
pub fn invalid_token() -> Self {
|
||||
|
@ -31,12 +33,16 @@ impl AppError {
|
|||
Self::new(ErrorKind::InvalidTokenAudience(audience.to_owned()))
|
||||
}
|
||||
|
||||
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
||||
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
||||
}
|
||||
|
||||
pub fn token_key() -> Self {
|
||||
Self::new(ErrorKind::TokenKey)
|
||||
}
|
||||
|
||||
pub fn is_duplicate_record(&self) -> bool {
|
||||
matches!(self.kind, ErrorKind::DuplicateRecord)
|
||||
matches!(self.kind, ErrorKind::DuplicateRecord(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,7 +63,7 @@ impl From<SqlxError> for AppError {
|
|||
match &other {
|
||||
SqlxError::Database(db_err) => {
|
||||
if let Some(err_code) = db_err.code() {
|
||||
map_db_error_code_to_error_kind(err_code)
|
||||
map_db_error_code_to_error_kind(err_code, db_err)
|
||||
} else {
|
||||
ErrorKind::Sqlx(other)
|
||||
}
|
||||
|
@ -77,10 +83,9 @@ impl Display for AppError {
|
|||
f,
|
||||
"Error occurred while initializing connection to database: {err}"
|
||||
),
|
||||
ErrorKind::DuplicateRecord => write!(
|
||||
f,
|
||||
"Error occurred while inserting a duplicate record into the database."
|
||||
),
|
||||
ErrorKind::DuplicateRecord(message) => {
|
||||
write!(f, "Duplicate database record: {message}")
|
||||
}
|
||||
ErrorKind::InvalidToken => write!(f, "The provided token is invalid."),
|
||||
ErrorKind::InvalidTokenAudience(audience) => write!(
|
||||
f,
|
||||
|
@ -107,7 +112,7 @@ enum ErrorKind {
|
|||
AppStartupError(io::Error),
|
||||
Database,
|
||||
DbMigration(MigrateError),
|
||||
DuplicateRecord,
|
||||
DuplicateRecord(String),
|
||||
InvalidToken,
|
||||
InvalidTokenAudience(String),
|
||||
MissingEnvironmentVariables(Vec<&'static str>),
|
||||
|
@ -117,16 +122,28 @@ enum ErrorKind {
|
|||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
if let ErrorKind::DuplicateRecord = &self.kind {}
|
||||
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
match &self.kind {
|
||||
&ErrorKind::DuplicateRecord(_) => (
|
||||
StatusCode::CONFLICT,
|
||||
ApiResponse::new_with_error(self).into_json_response(),
|
||||
),
|
||||
&ErrorKind::InvalidToken | &ErrorKind::InvalidTokenAudience(_) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
ApiResponse::new_with_error(self).into_json_response(),
|
||||
),
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiResponse::new_with_error(self).into_json_response(),
|
||||
),
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_db_error_code_to_error_kind(code: Cow<str>) -> ErrorKind {
|
||||
fn map_db_error_code_to_error_kind(code: Cow<str>, db_err: &Box<dyn DatabaseError>) -> ErrorKind {
|
||||
const UNIQUE_CONSTRAINT_VIOLATION: &str = "23505";
|
||||
if code == UNIQUE_CONSTRAINT_VIOLATION {
|
||||
ErrorKind::DuplicateRecord
|
||||
ErrorKind::DuplicateRecord(db_err.to_string())
|
||||
} else {
|
||||
ErrorKind::Database
|
||||
}
|
||||
|
|
11
api/src/requests/auth/login/handler.rs
Normal file
11
api/src/requests/auth/login/handler.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use axum::{
|
||||
debug_handler,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn auth_login_post_handler() -> Result<Response, AppError> {
|
||||
Ok(().into_response())
|
||||
}
|
3
api/src/requests/auth/login/mod.rs
Normal file
3
api/src/requests/auth/login/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod handler;
|
||||
|
||||
pub use handler::*;
|
13
api/src/requests/auth/mod.rs
Normal file
13
api/src/requests/auth/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{routing::post, Router};
|
||||
use login::auth_login_post_handler;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod login;
|
||||
|
||||
pub fn requests(state: AppState) -> Router {
|
||||
Router::new().route(
|
||||
"/auth/login",
|
||||
post(auth_login_post_handler).with_state(state),
|
||||
)
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod auth;
|
||||
mod user;
|
||||
|
||||
use std::time::Duration;
|
||||
|
@ -67,9 +68,10 @@ pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {
|
|||
}
|
||||
});
|
||||
|
||||
let app_state = AppState::new(pool, env);
|
||||
let state = AppState::new(pool, env);
|
||||
let app = Router::new()
|
||||
.merge(user::requests(app_state.clone()))
|
||||
.merge(user::requests(state.clone()))
|
||||
.merge(auth::requests(state.clone()))
|
||||
.layer(logging_layer);
|
||||
|
||||
info!("API started successfully.");
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::mpsc::Sender;
|
|||
|
||||
use crate::{
|
||||
db::{insert_new_user, DbPool, NewUserEntity, UserEntity},
|
||||
models::ApiResponse,
|
||||
models::{ApiResponse, AppError},
|
||||
requests::AppState,
|
||||
services::{auth_token::generate_new_user_token, hash_string, UserConfirmationMessage},
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ use super::models::{UserRegistrationRequest, UserRegistrationResponse};
|
|||
pub async fn user_registration_post_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<UserRegistrationRequest>,
|
||||
) -> Result<Response, Response> {
|
||||
) -> Result<Response, AppError> {
|
||||
let env = state.env();
|
||||
|
||||
register_new_user_request(
|
||||
|
@ -41,7 +41,7 @@ async fn register_new_user_request(
|
|||
signing_key: &SymmetricKey<V4>,
|
||||
send_verification_email: bool,
|
||||
email_sender: &Sender<UserConfirmationMessage>,
|
||||
) -> Result<Response, Response> {
|
||||
) -> Result<Response, AppError> {
|
||||
debug!(?body, send_verification_email);
|
||||
|
||||
let UserRegistrationRequest {
|
||||
|
@ -60,12 +60,18 @@ async fn register_new_user_request(
|
|||
name,
|
||||
};
|
||||
|
||||
let UserEntity { id: user_id, name, email , ..} = insert_new_user(pool, new_user).await
|
||||
.map_err(|err| {
|
||||
let UserEntity {
|
||||
id: user_id,
|
||||
name,
|
||||
email,
|
||||
..
|
||||
} = insert_new_user(pool, new_user).await.map_err(|err| {
|
||||
if err.is_duplicate_record() {
|
||||
(StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response()
|
||||
AppError::duplicate_record(
|
||||
"There is already an account associated with this username or email address.",
|
||||
)
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response()
|
||||
err
|
||||
}
|
||||
})?;
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ use verify::user_verification_get_handler;
|
|||
|
||||
use super::AppState;
|
||||
|
||||
pub fn requests(app_state: AppState) -> Router {
|
||||
pub fn requests(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/user", post(user_registration_post_handler))
|
||||
.route("/user/:user_id/verify", get(user_verification_get_handler))
|
||||
.with_state(app_state.clone())
|
||||
.with_state(state.clone())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use tracing::{debug, error};
|
|||
|
||||
use crate::{
|
||||
db::{verify_user, DbPool},
|
||||
models::ApiResponse,
|
||||
models::{ApiResponse, AppError},
|
||||
requests::AppState,
|
||||
services::auth_token::verify_token,
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ pub async fn user_verification_get_handler(
|
|||
State(state): State<AppState>,
|
||||
Path(user_id): Path<i32>,
|
||||
Query(query): Query<UserVerifyGetParams>,
|
||||
) -> Result<Response, Response> {
|
||||
) -> Result<Response, AppError> {
|
||||
let pool = state.pool();
|
||||
let env = state.env();
|
||||
|
||||
|
@ -36,7 +36,7 @@ async fn verify_new_user_request(
|
|||
user_id: i32,
|
||||
verification_token: String,
|
||||
token_key: &SymmetricKey<V4>,
|
||||
) -> Result<Response, Response> {
|
||||
) -> Result<Response, AppError> {
|
||||
debug!(user_id);
|
||||
|
||||
let validation_rules = {
|
||||
|
@ -52,13 +52,11 @@ async fn verify_new_user_request(
|
|||
Some(validation_rules),
|
||||
)
|
||||
.map(|_| UserVerifyGetResponse::new(token_key, user_id))
|
||||
.inspect_err(|err| error!(?err))
|
||||
.map_err(|err| err.into_response())?;
|
||||
.inspect_err(|err| error!(?err))?;
|
||||
|
||||
verify_user(pool, user_id)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err))
|
||||
.map_err(|err| err.into_response())?;
|
||||
.inspect_err(|err| error!(?err))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
|
|
Loading…
Add table
Reference in a new issue