| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | use axum::http::StatusCode; |
| 14 | use axum::response::{Html, IntoResponse, Response}; |
| 15 | use maud::{DOCTYPE, Markup, html}; |
| 16 | |
| 17 | |
| 18 | #[derive(Debug)] |
| 19 | pub enum AppError { |
| 20 | |
| 21 | |
| 22 | NotFound, |
| 23 | |
| 24 | Unauthorized, |
| 25 | |
| 26 | Forbidden, |
| 27 | |
| 28 | BadRequest(String), |
| 29 | |
| 30 | Internal(String), |
| 31 | } |
| 32 | |
| 33 | |
| 34 | pub type AppResult<T> = Result<T, AppError>; |
| 35 | |
| 36 | impl AppError { |
| 37 | |
| 38 | pub fn internal(err: impl std::fmt::Display) -> Self { |
| 39 | Self::Internal(err.to_string()) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | impl From<store::StoreError> for AppError { |
| 44 | fn from(err: store::StoreError) -> Self { |
| 45 | Self::Internal(err.to_string()) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | impl From<git::GitError> for AppError { |
| 50 | fn from(err: git::GitError) -> Self { |
| 51 | Self::internal(err) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | impl IntoResponse for AppError { |
| 56 | fn into_response(self) -> Response { |
| 57 | let (status, heading, detail): (StatusCode, &str, String) = match self { |
| 58 | AppError::NotFound => ( |
| 59 | StatusCode::NOT_FOUND, |
| 60 | "Not found", |
| 61 | "The page you were looking for does not exist.".to_string(), |
| 62 | ), |
| 63 | AppError::Unauthorized => ( |
| 64 | StatusCode::UNAUTHORIZED, |
| 65 | "Sign in required", |
| 66 | "You need to sign in to view this page.".to_string(), |
| 67 | ), |
| 68 | AppError::Forbidden => ( |
| 69 | StatusCode::FORBIDDEN, |
| 70 | "Forbidden", |
| 71 | "You do not have access to this page.".to_string(), |
| 72 | ), |
| 73 | AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "Bad request", msg), |
| 74 | AppError::Internal(detail) => { |
| 75 | |
| 76 | let id = correlation_id(); |
| 77 | tracing::error!(error.id = %id, error.detail = %detail, "internal error"); |
| 78 | ( |
| 79 | StatusCode::INTERNAL_SERVER_ERROR, |
| 80 | "Something went wrong", |
| 81 | format!("An internal error occurred. Reference: {id}"), |
| 82 | ) |
| 83 | } |
| 84 | }; |
| 85 | ( |
| 86 | status, |
| 87 | Html(error_page(status.as_u16(), heading, &detail).into_string()), |
| 88 | ) |
| 89 | .into_response() |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | |
| 94 | |
| 95 | fn correlation_id() -> String { |
| 96 | use std::time::{SystemTime, UNIX_EPOCH}; |
| 97 | let nanos = SystemTime::now() |
| 98 | .duration_since(UNIX_EPOCH) |
| 99 | .map_or(0, |d| d.as_nanos()); |
| 100 | format!("{nanos:x}") |
| 101 | } |
| 102 | |
| 103 | |
| 104 | fn error_page(code: u16, heading: &str, detail: &str) -> Markup { |
| 105 | html! { |
| 106 | (DOCTYPE) |
| 107 | html lang="en" { |
| 108 | head { |
| 109 | meta charset="utf-8"; |
| 110 | meta name="viewport" content="width=device-width, initial-scale=1"; |
| 111 | title { (code) " — " (heading) } |
| 112 | link rel="stylesheet" href="/assets/base.css"; |
| 113 | link rel="stylesheet" href="/theme.css"; |
| 114 | } |
| 115 | body { |
| 116 | div class="error-page" { |
| 117 | div class="code" { (code) } |
| 118 | h1 { (heading) } |
| 119 | p class="muted" { (detail) } |
| 120 | p { a href="/" { "Return home" } } |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | } |