allowthem_server/
browser_error.rs1use axum::http::StatusCode;
2use axum::response::{Html, IntoResponse, Response};
3
4const 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
30fn 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("&"),
37 '<' => out.push_str("<"),
38 '>' => out.push_str(">"),
39 '"' => out.push_str("""),
40 '\'' => out.push_str("'"),
41 _ => out.push(ch),
42 }
43 }
44 out
45}
46
47pub 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}