diff --git a/api/src/models/api_response.rs b/api/src/models/api_response.rs index 69267dd..9fb0c73 100644 --- a/api/src/models/api_response.rs +++ b/api/src/models/api_response.rs @@ -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 { @@ -12,7 +16,7 @@ pub struct ApiResponse { pub data: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option<&'static str>, + pub error: Option>, } impl ApiResponse { @@ -24,14 +28,6 @@ impl ApiResponse { } } - pub fn _new_empty() -> ApiResponse { - Self { - meta: None, - data: None, - error: None, - } - } - pub fn _new_with_metadata(data: T, meta: ApiResponseMetadata) -> ApiResponse { Self { meta: Some(meta), @@ -46,11 +42,27 @@ impl ApiResponse { } 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()), } } } diff --git a/api/src/models/error.rs b/api/src/models/error.rs index 4131187..2df809d 100644 --- a/api/src/models/error.rs +++ b/api/src/models/error.rs @@ -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 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) -> ErrorKind { +fn map_db_error_code_to_error_kind(code: Cow, db_err: &Box) -> ErrorKind { const UNIQUE_CONSTRAINT_VIOLATION: &str = "23505"; if code == UNIQUE_CONSTRAINT_VIOLATION { - ErrorKind::DuplicateRecord + ErrorKind::DuplicateRecord(db_err.to_string()) } else { ErrorKind::Database } diff --git a/api/src/requests/auth/login/handler.rs b/api/src/requests/auth/login/handler.rs new file mode 100644 index 0000000..f842d9b --- /dev/null +++ b/api/src/requests/auth/login/handler.rs @@ -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 { + Ok(().into_response()) +} diff --git a/api/src/requests/auth/login/mod.rs b/api/src/requests/auth/login/mod.rs new file mode 100644 index 0000000..7298495 --- /dev/null +++ b/api/src/requests/auth/login/mod.rs @@ -0,0 +1,3 @@ +mod handler; + +pub use handler::*; diff --git a/api/src/requests/auth/mod.rs b/api/src/requests/auth/mod.rs new file mode 100644 index 0000000..4a0a2f2 --- /dev/null +++ b/api/src/requests/auth/mod.rs @@ -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), + ) +} diff --git a/api/src/requests/mod.rs b/api/src/requests/mod.rs index baa7aa9..c20fa62 100644 --- a/api/src/requests/mod.rs +++ b/api/src/requests/mod.rs @@ -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."); diff --git a/api/src/requests/user/create/handler.rs b/api/src/requests/user/create/handler.rs index ec949ba..c19e854 100644 --- a/api/src/requests/user/create/handler.rs +++ b/api/src/requests/user/create/handler.rs @@ -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, Json(request): Json, -) -> Result { +) -> Result { let env = state.env(); register_new_user_request( @@ -41,7 +41,7 @@ async fn register_new_user_request( signing_key: &SymmetricKey, send_verification_email: bool, email_sender: &Sender, -) -> Result { +) -> Result { debug!(?body, send_verification_email); let UserRegistrationRequest { @@ -60,14 +60,20 @@ async fn register_new_user_request( name, }; - 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() - } 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() - } - })?; + let UserEntity { + id: user_id, + name, + email, + .. + } = 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); diff --git a/api/src/requests/user/mod.rs b/api/src/requests/user/mod.rs index 175d394..2cce196 100644 --- a/api/src/requests/user/mod.rs +++ b/api/src/requests/user/mod.rs @@ -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()) } diff --git a/api/src/requests/user/verify/handler.rs b/api/src/requests/user/verify/handler.rs index a6e183e..81c49cc 100644 --- a/api/src/requests/user/verify/handler.rs +++ b/api/src/requests/user/verify/handler.rs @@ -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, Path(user_id): Path, Query(query): Query, -) -> Result { +) -> Result { 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, -) -> Result { +) -> Result { 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,