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 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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
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;
|
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.");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue