Display an error when attempting to log in to an account that doesn't exist

This commit is contained in:
Z. Charles Dziura 2024-10-06 07:14:44 -04:00
parent 680114f467
commit e24cf5c0b8
5 changed files with 67 additions and 17 deletions

View file

@ -53,30 +53,32 @@ pub async fn insert_new_user(
.bind(name) .bind(name)
.fetch_one(pool).await .fetch_one(pool).await
.map_err(|err| { .map_err(|err| {
error!(%err, record = ?new_user, "Cannot insert new user record"); error!(?err, record = ?new_user, "Cannot insert new user record");
AppError::from(err) AppError::from(err)
}) })
} }
#[derive(Debug, FromRow)] #[derive(Debug, FromRow)]
pub struct UserIdAndHashedPassword { pub struct UserAndHashedPassword {
pub id: i32, pub id: i32,
pub username: String,
pub name: String,
pub password: String, pub password: String,
} }
pub async fn get_username_and_password_by_username( pub async fn get_username_and_password_by_username(
pool: &DbPool, pool: &DbPool,
username: String, username: String,
) -> Result<UserIdAndHashedPassword, AppError> { ) -> Result<UserAndHashedPassword, AppError> {
sqlx::query_as::<_, UserIdAndHashedPassword>( sqlx::query_as::<_, UserAndHashedPassword>(
"SELECT id, password FROM public.user WHERE username = $1;", "SELECT id, username, name, password FROM public.user WHERE username = $1;",
) )
.bind(username) .bind(username)
.fetch_one(pool) .fetch_one(pool)
.await .await
.map_err(|err| { .map_err(|err| {
error!(%err, "Unable to find user"); error!(?err, "Unable to find user");
AppError::from(err) AppError::from(err)
}) })
} }
@ -87,7 +89,7 @@ pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
.execute(pool) .execute(pool)
.await .await
.map_err(|err| { .map_err(|err| {
error!(%err, user_id, "Error verifying user"); error!(?err, user_id, "Error verifying user");
AppError::from(err) AppError::from(err)
}) })
.map(|_| ()) .map(|_| ())

View file

@ -81,6 +81,7 @@ impl From<MigrateError> for AppError {
impl From<SqlxError> for AppError { impl From<SqlxError> for AppError {
fn from(other: SqlxError) -> Self { fn from(other: SqlxError) -> Self {
match &other { match &other {
SqlxError::RowNotFound => ErrorKind::NoDbRecordFound,
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, db_err) map_db_error_code_to_error_kind(err_code, db_err)
@ -137,6 +138,7 @@ impl Display for AppError {
f, f,
"Cannot retrieve session: missing required field: {field}" "Cannot retrieve session: missing required field: {field}"
), ),
ErrorKind::NoDbRecordFound => write!(f, "No database record found"),
ErrorKind::NoSessionFound => write!(f, "No session found"), ErrorKind::NoSessionFound => write!(f, "No session found"),
ErrorKind::Sqlx(err) => write!(f, "{err}"), ErrorKind::Sqlx(err) => write!(f, "{err}"),
ErrorKind::TokenKey => write!( ErrorKind::TokenKey => write!(
@ -161,6 +163,7 @@ enum ErrorKind {
InvalidToken, InvalidToken,
MissingEnvironmentVariables(Vec<&'static str>), MissingEnvironmentVariables(Vec<&'static str>),
MissingSessionField(&'static str), MissingSessionField(&'static str),
NoDbRecordFound,
NoSessionFound, NoSessionFound,
Sqlx(SqlxError), Sqlx(SqlxError),
TokenKey, TokenKey,
@ -181,6 +184,10 @@ impl IntoResponse for AppError {
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
ApiResponse::new_with_error(self).into_json_response(), ApiResponse::new_with_error(self).into_json_response(),
), ),
&ErrorKind::NoDbRecordFound => (
StatusCode::NOT_FOUND,
ApiResponse::new_with_error(self).into_json_response(),
),
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
ApiResponse::new_with_error(self).into_json_response(), ApiResponse::new_with_error(self).into_json_response(),

View file

@ -4,13 +4,21 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json, Json,
}; };
use http::StatusCode;
use pasetors::{keys::SymmetricKey, version4::V4};
use tracing::debug; use tracing::debug;
use crate::{ use crate::{
db::{get_username_and_password_by_username, DbPool, UserIdAndHashedPassword}, db::{get_username_and_password_by_username, DbPool, UserAndHashedPassword},
models::AppError, models::{ApiResponse, AppError},
requests::AppState, requests::{
services::verify_password, auth::login::models::{AuthLoginResponse, AuthLoginTokenData},
AppState,
},
services::{
auth_token::{generate_access_token, generate_auth_token},
verify_password,
},
}; };
use super::models::AuthLoginRequest; use super::models::AuthLoginRequest;
@ -21,19 +29,50 @@ pub async fn auth_login_post_handler(
Json(body): Json<AuthLoginRequest>, Json(body): Json<AuthLoginRequest>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
let pool = state.db_pool(); let pool = state.db_pool();
auth_login_request(pool, body).await let token_key = state.env().token_key();
auth_login_request(pool, token_key, body).await
} }
async fn auth_login_request(pool: &DbPool, body: AuthLoginRequest) -> Result<Response, AppError> { async fn auth_login_request(
pool: &DbPool,
token_key: &SymmetricKey<V4>,
body: AuthLoginRequest,
) -> Result<Response, AppError> {
debug!(?body); debug!(?body);
let AuthLoginRequest { username, password } = body; let AuthLoginRequest { username, password } = body;
let UserIdAndHashedPassword { let UserAndHashedPassword {
id: _id, id: user_id,
username,
name,
password: hashed_password, password: hashed_password,
} = get_username_and_password_by_username(pool, username).await?; } = get_username_and_password_by_username(pool, username).await?;
verify_password(password, hashed_password)?; verify_password(password, hashed_password)?;
Ok(().into_response()) let (access_token, _access_token_id, access_token_expiration) =
generate_access_token(token_key, user_id);
let (auth_token, _auth_token_id, auth_token_expiration) =
generate_auth_token(token_key, user_id);
let response = AuthLoginResponse {
user_id,
username,
name,
access: AuthLoginTokenData {
token: access_token,
expiration: access_token_expiration,
},
auth: AuthLoginTokenData {
token: auth_token,
expiration: auth_token_expiration,
},
};
Ok((
StatusCode::OK,
ApiResponse::new(response).into_json_response(),
)
.into_response())
} }

View file

@ -2,4 +2,4 @@ mod request;
mod response; mod response;
pub use request::*; pub use request::*;
// pub use response::*; pub use response::*;

View file

@ -6,6 +6,8 @@ use serde::Serialize;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AuthLoginResponse { pub struct AuthLoginResponse {
pub user_id: i32, pub user_id: i32,
pub username: String,
pub name: String,
pub access: AuthLoginTokenData, pub access: AuthLoginTokenData,
pub auth: AuthLoginTokenData, pub auth: AuthLoginTokenData,
} }