Refactor admin routes to use new AppError struct in responses
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use crate::{
|
||||
routes::AdminError, session_state::TypedSession, telemetry::spawn_blocking_with_tracing,
|
||||
routes::{AdminError, AppError},
|
||||
session_state::TypedSession,
|
||||
telemetry::spawn_blocking_with_tracing,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use argon2::{
|
||||
@@ -135,7 +137,7 @@ pub async fn require_auth(
|
||||
session: TypedSession,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AdminError> {
|
||||
) -> Result<Response, AppError> {
|
||||
let user_id = session
|
||||
.get_user_id()
|
||||
.await
|
||||
|
||||
@@ -8,7 +8,10 @@ mod subscriptions_confirm;
|
||||
|
||||
pub use admin::*;
|
||||
use askama::Template;
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::{
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
pub use health_check::*;
|
||||
pub use home::*;
|
||||
pub use login::*;
|
||||
@@ -17,6 +20,62 @@ use reqwest::StatusCode;
|
||||
pub use subscriptions::*;
|
||||
pub use subscriptions_confirm::*;
|
||||
|
||||
use crate::templates::MessageTemplate;
|
||||
|
||||
#[derive(thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("An unexpected error was encountered.")]
|
||||
UnexpectedError(#[from] anyhow::Error),
|
||||
#[error("A validation error happened.")]
|
||||
ValidationError(#[source] anyhow::Error),
|
||||
#[error("An authentication is required.")]
|
||||
NotAuthenticated,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
error_chain_fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
tracing::error!("{:?}", self);
|
||||
|
||||
match &self {
|
||||
AppError::UnexpectedError(_) => {
|
||||
let template = MessageTemplate::Error {
|
||||
message: "An internal server error occured.".into(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AppError::ValidationError(error) => {
|
||||
let template = MessageTemplate::Error {
|
||||
message: error.to_string(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AppError::NotAuthenticated => {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("HX-Redirect", "/login".parse().unwrap());
|
||||
(StatusCode::UNAUTHORIZED, headers).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdminError> for AppError {
|
||||
fn from(value: AdminError) -> Self {
|
||||
match value {
|
||||
AdminError::UnexpectedError(error) => AppError::UnexpectedError(error),
|
||||
AdminError::NotAuthenticated => AppError::NotAuthenticated,
|
||||
AdminError::ChangePassword(s) => AppError::ValidationError(anyhow::anyhow!(s)),
|
||||
AdminError::Publish(e) => AppError::ValidationError(e),
|
||||
AdminError::Idempotency(s) => AppError::UnexpectedError(anyhow::anyhow!(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/404.html")]
|
||||
struct NotFoundTemplate;
|
||||
|
||||
@@ -4,19 +4,12 @@ mod logout;
|
||||
mod newsletters;
|
||||
mod posts;
|
||||
|
||||
use crate::{routes::error_chain_fmt, templates::MessageTemplate};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Json,
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use crate::routes::error_chain_fmt;
|
||||
pub use change_password::*;
|
||||
pub use dashboard::*;
|
||||
pub use logout::*;
|
||||
pub use newsletters::*;
|
||||
pub use posts::*;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[derive(thiserror::Error)]
|
||||
pub enum AdminError {
|
||||
@@ -37,45 +30,3 @@ impl std::fmt::Debug for AdminError {
|
||||
error_chain_fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AdminError {
|
||||
fn into_response(self) -> Response {
|
||||
#[derive(serde::Serialize)]
|
||||
struct ErrorResponse<'a> {
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
tracing::error!("{:?}", self);
|
||||
|
||||
match &self {
|
||||
AdminError::UnexpectedError(_) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
message: "An internal server error occured.",
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
AdminError::NotAuthenticated => {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("HX-Redirect", "/login".parse().unwrap());
|
||||
headers.insert("Location", "/login".parse().unwrap());
|
||||
(StatusCode::SEE_OTHER, headers).into_response()
|
||||
}
|
||||
AdminError::ChangePassword(e) => {
|
||||
let template = MessageTemplate::Error {
|
||||
message: e.to_owned(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AdminError::Publish(e) => {
|
||||
let template = MessageTemplate::Error {
|
||||
message: e.to_string(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AdminError::Idempotency(e) => {
|
||||
(StatusCode::BAD_REQUEST, Json(ErrorResponse { message: e })).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
authentication::{self, AuthenticatedUser, Credentials, validate_credentials},
|
||||
routes::AdminError,
|
||||
routes::{AdminError, AppError},
|
||||
startup::AppState,
|
||||
templates::MessageTemplate,
|
||||
};
|
||||
@@ -25,7 +25,7 @@ pub async fn change_password(
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Form(form): Form<PasswordFormData>,
|
||||
) -> Result<Response, AdminError> {
|
||||
) -> Result<Response, AppError> {
|
||||
let credentials = Credentials {
|
||||
username,
|
||||
password: form.current_password,
|
||||
@@ -33,16 +33,15 @@ pub async fn change_password(
|
||||
if form.new_password.expose_secret() != form.new_password_check.expose_secret() {
|
||||
Err(AdminError::ChangePassword(
|
||||
"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(),
|
||||
))
|
||||
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))
|
||||
Err(AdminError::ChangePassword(e).into())
|
||||
} else {
|
||||
authentication::change_password(user_id, form.new_password, &connection_pool)
|
||||
.await
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{routes::AdminError, session_state::TypedSession};
|
||||
use crate::session_state::TypedSession;
|
||||
use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[tracing::instrument(name = "Logging out", skip(session))]
|
||||
pub async fn logout(session: TypedSession) -> Result<Response, AdminError> {
|
||||
pub async fn logout(session: TypedSession) -> Response {
|
||||
session.clear().await;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("HX-Redirect", "/login".parse().unwrap());
|
||||
Ok((StatusCode::OK, headers).into_response())
|
||||
(StatusCode::OK, headers).into_response()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
authentication::AuthenticatedUser,
|
||||
idempotency::{IdempotencyKey, save_response, try_processing},
|
||||
routes::AdminError,
|
||||
routes::{AdminError, AppError},
|
||||
startup::AppState,
|
||||
templates::MessageTemplate,
|
||||
};
|
||||
@@ -75,7 +75,7 @@ pub async fn publish_newsletter(
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<BodyData>,
|
||||
) -> Result<Response, AdminError> {
|
||||
) -> Result<Response, AppError> {
|
||||
validate_form(&form).map_err(|e| AdminError::Publish(anyhow::anyhow!(e)))?;
|
||||
|
||||
let idempotency_key: IdempotencyKey = form
|
||||
@@ -104,9 +104,10 @@ pub async fn publish_newsletter(
|
||||
);
|
||||
let template = MessageTemplate::Success { message };
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
save_response(transaction, &idempotency_key, user_id, response)
|
||||
let response = save_response(transaction, &idempotency_key, user_id, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)
|
||||
.map_err(AdminError::UnexpectedError)?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn validate_form(form: &BodyData) -> Result<(), &'static str> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
authentication::AuthenticatedUser,
|
||||
idempotency::{IdempotencyKey, save_response, try_processing},
|
||||
routes::{AdminError, enqueue_delivery_tasks, insert_newsletter_issue},
|
||||
routes::{AdminError, AppError, enqueue_delivery_tasks, insert_newsletter_issue},
|
||||
startup::AppState,
|
||||
templates::MessageTemplate,
|
||||
};
|
||||
@@ -38,7 +38,7 @@ pub async fn create_post(
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<CreatePostForm>,
|
||||
) -> Result<Response, AdminError> {
|
||||
) -> Result<Response, AppError> {
|
||||
validate_form(&form).map_err(AdminError::Publish)?;
|
||||
|
||||
let idempotency_key: IdempotencyKey = form
|
||||
@@ -65,16 +65,14 @@ pub async fn create_post(
|
||||
.await
|
||||
.context("Failed to enqueue delivery tasks.")?;
|
||||
|
||||
// Send emails with unique identifiers that contains link to blog post with special param
|
||||
// Get handpoint that returns the post and mark the email as opened
|
||||
|
||||
let template = MessageTemplate::Success {
|
||||
message: "Your new post has been saved. Subscribers will be notified.".into(),
|
||||
};
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
save_response(transaction, &idempotency_key, user_id, response)
|
||||
let response = save_response(transaction, &idempotency_key, user_id, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)
|
||||
.map_err(AdminError::UnexpectedError)?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
@@ -116,5 +114,6 @@ pub async fn create_newsletter(
|
||||
content: &str,
|
||||
_post_id: &Uuid,
|
||||
) -> Result<Uuid, sqlx::Error> {
|
||||
// We need to send a special link with a unique ID to determine if the user clicked it or not.
|
||||
insert_newsletter_issue(transaction, title, content, content).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user