Error handling refactor and 500 page/message templates

This commit is contained in:
Alphonse Paix
2025-09-20 04:06:48 +02:00
parent 7971095227
commit d85879a004
14 changed files with 223 additions and 201 deletions

View File

@@ -4,7 +4,12 @@ mod logout;
mod newsletters;
mod posts;
use crate::routes::error_chain_fmt;
use crate::{
authentication::AuthenticatedUser,
routes::{AppError, error_chain_fmt},
session_state::TypedSession,
};
use axum::{extract::Request, middleware::Next, response::Response};
pub use change_password::*;
pub use dashboard::*;
pub use logout::*;
@@ -30,3 +35,28 @@ impl std::fmt::Debug for AdminError {
error_chain_fmt(self, f)
}
}
pub async fn require_auth(
session: TypedSession,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let user_id = session
.get_user_id()
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?
.ok_or(AdminError::NotAuthenticated)?;
let username = session
.get_username()
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?
.ok_or(AdminError::UnexpectedError(anyhow::anyhow!(
"Could not find username in session."
)))?;
request
.extensions_mut()
.insert(AuthenticatedUser { user_id, username });
Ok(next.run(request).await)
}

View File

@@ -1,5 +1,5 @@
use crate::{
authentication::{self, AuthenticatedUser, Credentials, validate_credentials},
authentication::{self, AuthError, AuthenticatedUser, Credentials, validate_credentials},
routes::{AdminError, AppError},
startup::AppState,
templates::MessageTemplate,
@@ -35,11 +35,14 @@ pub async fn change_password(
"You entered two different passwords - the field values must match.".to_string(),
)
.into())
} else if validate_credentials(credentials, &connection_pool)
.await
.is_err()
{
Err(AdminError::ChangePassword("The current password is incorrect.".to_string()).into())
} else if let Err(e) = validate_credentials(credentials, &connection_pool).await {
match e {
AuthError::UnexpectedError(error) => Err(AdminError::UnexpectedError(error).into()),
AuthError::InvalidCredentials(_) => Err(AdminError::ChangePassword(
"The current password is incorrect.".to_string(),
)
.into()),
}
} else if let Err(e) = verify_password(form.new_password.expose_secret()) {
Err(AdminError::ChangePassword(e).into())
} else {

View File

@@ -84,7 +84,7 @@ pub async fn insert_post(
title: &str,
content: &str,
author: &Uuid,
) -> Result<Uuid, AdminError> {
) -> Result<Uuid, sqlx::Error> {
let post_id = Uuid::new_v4();
let query = sqlx::query!(
r#"
@@ -97,10 +97,7 @@ pub async fn insert_post(
content,
Utc::now()
);
transaction
.execute(query)
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?;
transaction.execute(query).await?;
Ok(post_id)
}

View File

@@ -1,13 +1,13 @@
use crate::{
authentication::{AuthError, Credentials, validate_credentials},
routes::error_chain_fmt,
authentication::{Credentials, validate_credentials},
routes::AppError,
session_state::TypedSession,
startup::AppState,
templates::MessageTemplate,
};
use anyhow::Context;
use askama::Template;
use axum::{
Form, Json,
Form,
extract::State,
http::HeaderMap,
response::{Html, IntoResponse, Response},
@@ -15,47 +15,6 @@ use axum::{
use axum::{http::StatusCode, response::Redirect};
use secrecy::SecretString;
#[derive(thiserror::Error)]
pub enum LoginError {
#[error("Something went wrong.")]
UnexpectedError(#[from] anyhow::Error),
#[error("Authentication failed.")]
AuthError(#[source] anyhow::Error),
}
impl std::fmt::Debug for LoginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl IntoResponse for LoginError {
fn into_response(self) -> Response {
#[derive(serde::Serialize)]
struct ErrorResponse<'a> {
message: &'a str,
}
tracing::error!("{:?}", self);
match &self {
LoginError::UnexpectedError(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
message: "An internal server error occured.",
}),
)
.into_response(),
LoginError::AuthError(e) => {
let template = MessageTemplate::Error {
message: e.to_string(),
};
Html(template.render().unwrap()).into_response()
}
}
}
}
#[derive(Template)]
#[template(path = "../templates/login.html")]
struct LoginTemplate;
@@ -66,11 +25,11 @@ pub struct LoginFormData {
password: SecretString,
}
pub async fn get_login(session: TypedSession) -> Result<Response, LoginError> {
pub async fn get_login(session: TypedSession) -> Result<Response, AppError> {
if session
.get_user_id()
.await
.map_err(|e| LoginError::UnexpectedError(e.into()))?
.context("Failed to retrieve user id from data store.")?
.is_some()
{
Ok(Redirect::to("/admin/dashboard").into_response())
@@ -85,39 +44,26 @@ pub async fn post_login(
connection_pool, ..
}): State<AppState>,
Form(form): Form<LoginFormData>,
) -> Result<Response, LoginError> {
) -> Result<Response, AppError> {
let credentials = Credentials {
username: form.username.clone(),
password: form.password,
};
tracing::Span::current().record("username", tracing::field::display(&credentials.username));
match validate_credentials(credentials, &connection_pool).await {
Err(e) => {
let e = match e {
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()),
AuthError::InvalidCredentials(_) => LoginError::AuthError(e.into()),
AuthError::NotAuthenticated => unreachable!(),
};
Err(e)
}
Ok(user_id) => {
tracing::Span::current().record("user_id", tracing::field::display(&user_id));
session
.renew()
.await
.map_err(|e| LoginError::UnexpectedError(e.into()))?;
session
.insert_user_id(user_id)
.await
.map_err(|e| LoginError::UnexpectedError(e.into()))?;
session
.insert_username(form.username)
.await
.map_err(|e| LoginError::UnexpectedError(e.into()))?;
let user_id = validate_credentials(credentials, &connection_pool).await?;
tracing::Span::current().record("user_id", tracing::field::display(&user_id));
let mut headers = HeaderMap::new();
headers.insert("HX-Redirect", "/admin/dashboard".parse().unwrap());
Ok((StatusCode::OK, headers).into_response())
}
}
session.renew().await.context("Failed to renew session.")?;
session
.insert_user_id(user_id)
.await
.context("Failed to insert user id in session data store.")?;
session
.insert_username(form.username)
.await
.context("Failed to insert username in session data store.")?;
let mut headers = HeaderMap::new();
headers.insert("HX-Redirect", "/admin/dashboard".parse().unwrap());
Ok((StatusCode::OK, headers).into_response())
}

View File

@@ -1,11 +1,11 @@
use crate::startup::AppState;
use crate::{routes::AppError, startup::AppState};
use anyhow::Context;
use askama::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Response},
};
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use sqlx::PgPool;
use uuid::Uuid;
@@ -40,17 +40,13 @@ pub async fn list_posts(
State(AppState {
connection_pool, ..
}): State<AppState>,
) -> Response {
match get_latest_posts(&connection_pool, 5).await {
Err(e) => {
tracing::error!("Could not fetch latest posts: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
Ok(posts) => {
let template = PostsTemplate { posts };
Html(template.render().unwrap()).into_response()
}
}
) -> Result<Response, AppError> {
let posts = get_latest_posts(&connection_pool, 5)
.await
.context("Could not fetch latest posts")
.map_err(AppError::unexpected_page)?;
let template = PostsTemplate { posts };
Ok(Html(template.render().unwrap()).into_response())
}
async fn get_latest_posts(connection_pool: &PgPool, n: i64) -> Result<Vec<PostEntry>, sqlx::Error> {
@@ -74,17 +70,13 @@ pub async fn see_post(
connection_pool, ..
}): State<AppState>,
Path(post_id): Path<Uuid>,
) -> Response {
match get_post(&connection_pool, post_id).await {
Err(e) => {
tracing::error!("Could not fetch post #{}: {}", post_id, e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
Ok(post) => {
let template = PostTemplate { post };
Html(template.render().unwrap()).into_response()
}
}
) -> Result<Response, AppError> {
let post = get_post(&connection_pool, post_id)
.await
.context(format!("Failed to fetch post #{}", post_id))
.map_err(AppError::unexpected_page)?;
let template = PostTemplate { post };
Ok(Html(template.render().unwrap()).into_response())
}
async fn get_post(connection_pool: &PgPool, post_id: Uuid) -> Result<PostEntry, sqlx::Error> {

View File

@@ -1,15 +1,15 @@
use crate::{
domain::{NewSubscriber, SubscriberEmail},
email_client::EmailClient,
routes::AppError,
startup::AppState,
templates::MessageTemplate,
};
use anyhow::Context;
use askama::Template;
use axum::{
Form, Json,
Form,
extract::State,
http::StatusCode,
response::{Html, IntoResponse, Response},
};
use chrono::Utc;
@@ -42,45 +42,6 @@ pub fn error_chain_fmt(
Ok(())
}
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
#[error("{0}")]
ValidationError(String),
}
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl IntoResponse for SubscribeError {
fn into_response(self) -> Response {
#[derive(serde::Serialize)]
struct ErrorResponse<'a> {
message: &'a str,
}
tracing::error!("{:?}", self);
match self {
SubscribeError::UnexpectedError(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
message: "An internal server error occured.",
}),
)
.into_response(),
SubscribeError::ValidationError(e) => {
let template = MessageTemplate::Error { message: e };
Html(template.render().unwrap()).into_response()
}
}
}
}
#[tracing::instrument(
name = "Adding a new subscriber",
skip(connection_pool, email_client, base_url, form),
@@ -96,13 +57,11 @@ pub async fn subscribe(
..
}): State<AppState>,
Form(form): Form<SubscriptionFormData>,
) -> Result<Response, SubscribeError> {
let new_subscriber = match form.try_into() {
Ok(new_sub) => new_sub,
Err(e) => {
return Err(SubscribeError::ValidationError(e));
}
};
) -> Result<Response, AppError> {
let new_subscriber = form
.try_into()
.context("Failed to parse subscription form data.")
.map_err(AppError::FormError)?;
let mut transaction = connection_pool
.begin()
.await
@@ -122,6 +81,7 @@ pub async fn subscribe(
)
.await
.context("Failed to send a confirmation email.")?;
transaction
.commit()
.await
@@ -214,7 +174,7 @@ pub struct SubscriptionFormData {
}
impl TryFrom<SubscriptionFormData> for NewSubscriber {
type Error = String;
type Error = anyhow::Error;
fn try_from(value: SubscriptionFormData) -> Result<Self, Self::Error> {
let email = SubscriberEmail::parse(value.email)?;