mod admin; mod health_check; mod home; mod login; mod posts; mod subscriptions; mod subscriptions_confirm; mod unsubscribe; pub use admin::*; use askama::Template; use axum::{ extract::FromRequestParts, http::{HeaderMap, request::Parts}, response::{Html, IntoResponse, Response}, }; pub use health_check::*; pub use home::*; pub use login::*; pub use posts::*; use rand::{Rng, distr::Alphanumeric}; use reqwest::StatusCode; use serde::de::DeserializeOwned; pub use subscriptions::*; pub use subscriptions_confirm::*; pub use unsubscribe::*; use crate::{ authentication::AuthError, templates::{HtmlTemplate, InternalErrorTemplate, MessageTemplate, NotFoundTemplate}, }; pub fn generate_token() -> String { let mut rng = rand::rng(); std::iter::repeat_with(|| rng.sample(Alphanumeric)) .map(char::from) .take(25) .collect() } fn error_chain_fmt(e: &impl std::error::Error, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "{}", e)?; let mut current = e.source(); while let Some(cause) = current { write!(f, "Caused by:\n\t{}", cause)?; current = cause.source(); if current.is_some() { writeln!(f)?; } } Ok(()) } #[derive(thiserror::Error)] pub enum AppError { #[error("An unexpected error was encountered.")] UnexpectedError { #[source] error: anyhow::Error, full_page: bool, }, #[error("A validation error happened.")] FormError(#[source] anyhow::Error), #[error("Authentication is required.")] NotAuthenticated, #[error("Handler extractor failed.")] Extractor(#[source] anyhow::Error), } impl From for AppError { fn from(value: anyhow::Error) -> Self { Self::UnexpectedError { error: value, full_page: false, } } } impl AppError { pub fn unexpected_page(error: anyhow::Error) -> Self { Self::UnexpectedError { error, full_page: true, } } pub fn unexpected_message(error: anyhow::Error) -> Self { Self::UnexpectedError { error, full_page: false, } } } 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 { error: _, full_page, } => { let html = if *full_page { Html(InternalErrorTemplate.render().unwrap()) } else { let template = MessageTemplate::Error { message: "An internal server error occured.".into(), }; Html(template.render().unwrap()) }; html.into_response() } AppError::FormError(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::OK, headers).into_response() } AppError::Extractor(_) => not_found_html(), } } } impl From for AppError { fn from(value: AdminError) -> Self { match value { AdminError::UnexpectedError(error) => AppError::unexpected_message(error), AdminError::NotAuthenticated => AppError::NotAuthenticated, AdminError::ChangePassword(s) => AppError::FormError(anyhow::anyhow!(s)), AdminError::Publish(e) => AppError::FormError(e), AdminError::Idempotency(s) => AppError::UnexpectedError { error: anyhow::anyhow!(s), full_page: false, }, } } } impl From for AppError { fn from(value: AuthError) -> Self { match value { AuthError::UnexpectedError(error) => AppError::unexpected_message(error), AuthError::InvalidCredentials(error) => { AppError::FormError(error.context("Invalid credentials.")) } } } } pub async fn not_found() -> Response { not_found_html() } pub fn not_found_html() -> Response { let template = HtmlTemplate(NotFoundTemplate); (StatusCode::NOT_FOUND, template).into_response() } pub struct Path(T); impl FromRequestParts for Path where T: DeserializeOwned + Send, S: Send + Sync, { type Rejection = AppError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { match axum::extract::Path::::from_request_parts(parts, state).await { Ok(value) => Ok(Self(value.0)), Err(rejection) => Err(AppError::Extractor(anyhow::anyhow!( "Path rejection: {:?}", rejection ))), } } } pub struct Query(pub T); impl FromRequestParts for Query where T: DeserializeOwned, S: Send + Sync, { type Rejection = AppError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { match axum::extract::Query::::from_request_parts(parts, state).await { Ok(value) => Ok(Self(value.0)), Err(rejection) => Err(AppError::Extractor(anyhow::anyhow!( "Query rejection: {:?}", rejection ))), } } }