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