Change requests to return an AppError on failure

This commit is contained in:
Z. Charles Dziura 2024-10-03 15:55:38 -04:00
parent 8e222ff2e9
commit 8d4a987f0d
9 changed files with 109 additions and 47 deletions

View file

@ -1,6 +1,10 @@
use std::borrow::Cow;
use axum::Json; use axum::Json;
use serde::Serialize; use serde::Serialize;
use super::AppError;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ApiResponse<T> { pub struct ApiResponse<T> {
@ -12,7 +16,7 @@ pub struct ApiResponse<T> {
pub data: Option<T>, pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<&'static str>, pub error: Option<Cow<'static, str>>,
} }
impl<T> ApiResponse<T> { 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> { pub fn _new_with_metadata(data: T, meta: ApiResponseMetadata) -> ApiResponse<T> {
Self { Self {
meta: Some(meta), meta: Some(meta),
@ -46,11 +42,27 @@ impl<T> ApiResponse<T> {
} }
impl ApiResponse<()> { impl ApiResponse<()> {
pub fn _new_empty() -> Self {
Self {
meta: None,
data: None,
error: None,
}
}
pub fn error(error: &'static str) -> Self { pub fn error(error: &'static str) -> Self {
Self { Self {
meta: None, meta: None,
data: 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()),
} }
} }
} }

View file

@ -2,7 +2,9 @@ use std::{borrow::Cow, error::Error, fmt::Display, io};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use http::StatusCode; use http::StatusCode;
use sqlx::{migrate::MigrateError, Error as SqlxError}; use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
use super::ApiResponse;
#[derive(Debug)] #[derive(Debug)]
pub struct AppError { pub struct AppError {
@ -18,8 +20,8 @@ impl AppError {
Self::new(ErrorKind::AppStartupError(error)) Self::new(ErrorKind::AppStartupError(error))
} }
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self { pub fn duplicate_record(message: &str) -> Self {
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars)) Self::new(ErrorKind::DuplicateRecord(message.to_owned()))
} }
pub fn invalid_token() -> Self { pub fn invalid_token() -> Self {
@ -31,12 +33,16 @@ impl AppError {
Self::new(ErrorKind::InvalidTokenAudience(audience.to_owned())) 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 { pub fn token_key() -> Self {
Self::new(ErrorKind::TokenKey) Self::new(ErrorKind::TokenKey)
} }
pub fn is_duplicate_record(&self) -> bool { 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 { match &other {
SqlxError::Database(db_err) => { SqlxError::Database(db_err) => {
if let Some(err_code) = db_err.code() { 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 { } else {
ErrorKind::Sqlx(other) ErrorKind::Sqlx(other)
} }
@ -77,10 +83,9 @@ impl Display for AppError {
f, f,
"Error occurred while initializing connection to database: {err}" "Error occurred while initializing connection to database: {err}"
), ),
ErrorKind::DuplicateRecord => write!( ErrorKind::DuplicateRecord(message) => {
f, write!(f, "Duplicate database record: {message}")
"Error occurred while inserting a duplicate record into the database." }
),
ErrorKind::InvalidToken => write!(f, "The provided token is invalid."), ErrorKind::InvalidToken => write!(f, "The provided token is invalid."),
ErrorKind::InvalidTokenAudience(audience) => write!( ErrorKind::InvalidTokenAudience(audience) => write!(
f, f,
@ -107,7 +112,7 @@ enum ErrorKind {
AppStartupError(io::Error), AppStartupError(io::Error),
Database, Database,
DbMigration(MigrateError), DbMigration(MigrateError),
DuplicateRecord, DuplicateRecord(String),
InvalidToken, InvalidToken,
InvalidTokenAudience(String), InvalidTokenAudience(String),
MissingEnvironmentVariables(Vec<&'static str>), MissingEnvironmentVariables(Vec<&'static str>),
@ -117,16 +122,28 @@ enum ErrorKind {
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
if let ErrorKind::DuplicateRecord = &self.kind {} match &self.kind {
&ErrorKind::DuplicateRecord(_) => (
StatusCode::INTERNAL_SERVER_ERROR.into_response() 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"; const UNIQUE_CONSTRAINT_VIOLATION: &str = "23505";
if code == UNIQUE_CONSTRAINT_VIOLATION { if code == UNIQUE_CONSTRAINT_VIOLATION {
ErrorKind::DuplicateRecord ErrorKind::DuplicateRecord(db_err.to_string())
} else { } else {
ErrorKind::Database ErrorKind::Database
} }

View 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())
}

View file

@ -0,0 +1,3 @@
mod handler;
pub use handler::*;

View 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),
)
}

View file

@ -1,3 +1,4 @@
mod auth;
mod user; mod user;
use std::time::Duration; 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() let app = Router::new()
.merge(user::requests(app_state.clone())) .merge(user::requests(state.clone()))
.merge(auth::requests(state.clone()))
.layer(logging_layer); .layer(logging_layer);
info!("API started successfully."); info!("API started successfully.");

View file

@ -2,7 +2,7 @@ use std::sync::mpsc::Sender;
use crate::{ use crate::{
db::{insert_new_user, DbPool, NewUserEntity, UserEntity}, db::{insert_new_user, DbPool, NewUserEntity, UserEntity},
models::ApiResponse, models::{ApiResponse, AppError},
requests::AppState, requests::AppState,
services::{auth_token::generate_new_user_token, hash_string, UserConfirmationMessage}, 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( pub async fn user_registration_post_handler(
State(state): State<AppState>, State(state): State<AppState>,
Json(request): Json<UserRegistrationRequest>, Json(request): Json<UserRegistrationRequest>,
) -> Result<Response, Response> { ) -> Result<Response, AppError> {
let env = state.env(); let env = state.env();
register_new_user_request( register_new_user_request(
@ -41,7 +41,7 @@ async fn register_new_user_request(
signing_key: &SymmetricKey<V4>, signing_key: &SymmetricKey<V4>,
send_verification_email: bool, send_verification_email: bool,
email_sender: &Sender<UserConfirmationMessage>, email_sender: &Sender<UserConfirmationMessage>,
) -> Result<Response, Response> { ) -> Result<Response, AppError> {
debug!(?body, send_verification_email); debug!(?body, send_verification_email);
let UserRegistrationRequest { let UserRegistrationRequest {
@ -60,14 +60,20 @@ async fn register_new_user_request(
name, name,
}; };
let UserEntity { id: user_id, name, email , ..} = insert_new_user(pool, new_user).await let UserEntity {
.map_err(|err| { id: user_id,
if err.is_duplicate_record() { name,
(StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response() email,
} 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() } = insert_new_user(pool, new_user).await.map_err(|err| {
} if err.is_duplicate_record() {
})?; AppError::duplicate_record(
"There is already an account associated with this username or email address.",
)
} else {
err
}
})?;
let (verification_token, expiration) = generate_new_user_token(signing_key, user_id); let (verification_token, expiration) = generate_new_user_token(signing_key, user_id);

View file

@ -11,9 +11,9 @@ use verify::user_verification_get_handler;
use super::AppState; use super::AppState;
pub fn requests(app_state: AppState) -> Router { pub fn requests(state: AppState) -> Router {
Router::new() Router::new()
.route("/user", post(user_registration_post_handler)) .route("/user", post(user_registration_post_handler))
.route("/user/:user_id/verify", get(user_verification_get_handler)) .route("/user/:user_id/verify", get(user_verification_get_handler))
.with_state(app_state.clone()) .with_state(state.clone())
} }

View file

@ -10,7 +10,7 @@ use tracing::{debug, error};
use crate::{ use crate::{
db::{verify_user, DbPool}, db::{verify_user, DbPool},
models::ApiResponse, models::{ApiResponse, AppError},
requests::AppState, requests::AppState,
services::auth_token::verify_token, services::auth_token::verify_token,
}; };
@ -22,7 +22,7 @@ pub async fn user_verification_get_handler(
State(state): State<AppState>, State(state): State<AppState>,
Path(user_id): Path<i32>, Path(user_id): Path<i32>,
Query(query): Query<UserVerifyGetParams>, Query(query): Query<UserVerifyGetParams>,
) -> Result<Response, Response> { ) -> Result<Response, AppError> {
let pool = state.pool(); let pool = state.pool();
let env = state.env(); let env = state.env();
@ -36,7 +36,7 @@ async fn verify_new_user_request(
user_id: i32, user_id: i32,
verification_token: String, verification_token: String,
token_key: &SymmetricKey<V4>, token_key: &SymmetricKey<V4>,
) -> Result<Response, Response> { ) -> Result<Response, AppError> {
debug!(user_id); debug!(user_id);
let validation_rules = { let validation_rules = {
@ -52,13 +52,11 @@ async fn verify_new_user_request(
Some(validation_rules), Some(validation_rules),
) )
.map(|_| UserVerifyGetResponse::new(token_key, user_id)) .map(|_| UserVerifyGetResponse::new(token_key, user_id))
.inspect_err(|err| error!(?err)) .inspect_err(|err| error!(?err))?;
.map_err(|err| err.into_response())?;
verify_user(pool, user_id) verify_user(pool, user_id)
.await .await
.inspect_err(|err| error!(?err)) .inspect_err(|err| error!(?err))?;
.map_err(|err| err.into_response())?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,