Skip to main content

allowthem_server/
browser_error.rs

1use axum::http::StatusCode;
2use axum::response::{Html, IntoResponse, Response};
3
4/// Minimal styled HTML used as a fallback when the template environment is
5/// not available (e.g. inside `IntoResponse` for `BrowserError`). Keeps the
6/// same visual tone as `error.html` without requiring a template render.
7const FALLBACK_ERROR_HTML: &str = r#"<!DOCTYPE html>
8<html lang="en">
9<head>
10  <meta charset="utf-8">
11  <meta name="viewport" content="width=device-width, initial-scale=1">
12  <title>{{TITLE}} — allowthem</title>
13  <link rel="stylesheet" href="/__allowthem/static/css/01-tokens.css">
14  <link rel="stylesheet" href="/__allowthem/static/css/02-base.css">
15  <link rel="stylesheet" href="/__allowthem/static/css/03-layout.css">
16  <link rel="stylesheet" href="/__allowthem/static/css/04-components.css">
17  <link rel="stylesheet" href="/__allowthem/static/css/05-utilities.css">
18</head>
19<body class="wf-auth" style="display:flex;align-items:center;justify-content:center;min-height:100vh">
20  <main class="wf-auth-form" style="max-width:460px;width:100%">
21    <div class="wf-auth-wrap">
22      <h1>{{TITLE}}</h1>
23      <p class="wf-auth-sub">{{MESSAGE}}</p>
24      <p class="wf-caption wf-mt-5"><a href="/">Return home</a></p>
25    </div>
26  </main>
27</body>
28</html>"#;
29
30/// Escape HTML-special characters to prevent XSS when interpolating
31/// into the static error page template.
32fn html_escape(s: &str) -> String {
33    let mut out = String::with_capacity(s.len());
34    for ch in s.chars() {
35        match ch {
36            '&' => out.push_str("&amp;"),
37            '<' => out.push_str("&lt;"),
38            '>' => out.push_str("&gt;"),
39            '"' => out.push_str("&quot;"),
40            '\'' => out.push_str("&#x27;"),
41            _ => out.push(ch),
42        }
43    }
44    out
45}
46
47/// Build a static error page by replacing placeholders in the fallback HTML.
48///
49/// Also available as `render_error_page` for use by other modules that need
50/// to produce styled error pages without access to the template environment.
51/// Values are HTML-escaped to prevent XSS.
52pub fn render_error_page(title: &str, message: &str) -> String {
53    FALLBACK_ERROR_HTML
54        .replace("{{TITLE}}", &html_escape(title))
55        .replace("{{MESSAGE}}", &html_escape(message))
56}
57
58#[derive(Debug)]
59pub enum BrowserError {
60    Template(minijinja::Error),
61    Auth(allowthem_core::AuthError),
62}
63
64impl From<minijinja::Error> for BrowserError {
65    fn from(err: minijinja::Error) -> Self {
66        BrowserError::Template(err)
67    }
68}
69
70impl From<allowthem_core::AuthError> for BrowserError {
71    fn from(err: allowthem_core::AuthError) -> Self {
72        BrowserError::Auth(err)
73    }
74}
75
76impl IntoResponse for BrowserError {
77    fn into_response(self) -> Response {
78        match self {
79            BrowserError::Template(e) => {
80                tracing::error!(error = %e, "template render failed");
81                let html = render_error_page(
82                    "Internal error",
83                    "Something went wrong while rendering this page.",
84                );
85                (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response()
86            }
87            BrowserError::Auth(allowthem_core::AuthError::NotFound) => {
88                let html = render_error_page(
89                    "Not found",
90                    "The page you are looking for could not be found.",
91                );
92                (StatusCode::NOT_FOUND, Html(html)).into_response()
93            }
94            BrowserError::Auth(allowthem_core::AuthError::Validation(msg)) => {
95                tracing::warn!(error = %msg, "validation error");
96                let html = render_error_page("Validation error", &msg);
97                (StatusCode::UNPROCESSABLE_ENTITY, Html(html)).into_response()
98            }
99            BrowserError::Auth(e) => {
100                tracing::error!(error = %e, "auth error");
101                let html = render_error_page(
102                    "Internal error",
103                    "Something went wrong. Please try again later.",
104                );
105                (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response()
106            }
107        }
108    }
109}