Skip to main content

allowthem_server/
browser_templates.rs

1use std::sync::Arc;
2
3use axum::response::Html;
4use minijinja::Environment;
5
6use crate::browser_error::BrowserError;
7
8const BASE_HTML: &str = include_str!("templates/base.html");
9const LOGIN_HTML: &str = include_str!("templates/login.html");
10const REGISTER_HTML: &str = include_str!("templates/register.html");
11const SETTINGS_HTML: &str = include_str!("templates/settings.html");
12const CONSENT_HTML: &str = include_str!("templates/consent.html");
13const FORGOT_PASSWORD_HTML: &str = include_str!("templates/forgot_password.html");
14const RESET_PASSWORD_HTML: &str = include_str!("templates/reset_password.html");
15const MFA_SETUP_HTML: &str = include_str!("templates/mfa_setup.html");
16const MFA_RECOVERY_HTML: &str = include_str!("templates/mfa_recovery.html");
17const MFA_CHALLENGE_HTML: &str = include_str!("templates/mfa_challenge.html");
18const MODELINE_PARTIAL: &str = include_str!("templates/_partials/_modeline.html");
19const FLASH_PARTIAL: &str = include_str!("templates/_partials/_flash.html");
20const SPLASH_PARTIAL: &str = include_str!("templates/_partials/_splash.html");
21const AUTH_SHELL_PARTIAL: &str = include_str!("templates/_partials/_auth_shell.html");
22const APP_SHELL_PARTIAL: &str = include_str!("templates/_partials/_app_shell.html");
23const SIDEBAR_NAV_PARTIAL: &str = include_str!("templates/_partials/_sidebar_nav.html");
24const AUTH_MACROS_PARTIAL: &str = include_str!("templates/_partials/_auth_macros.html");
25const AUTH_OOB_HEAD_PARTIAL: &str = include_str!("templates/_partials/_auth_oob_head.html");
26const AUTH_MAIN_LOGIN_PARTIAL: &str = include_str!("templates/_partials/_auth_main_login.html");
27const AUTH_MAIN_REGISTER_PARTIAL: &str =
28    include_str!("templates/_partials/_auth_main_register.html");
29const AUTH_MAIN_FORGOT_PW_PARTIAL: &str =
30    include_str!("templates/_partials/_auth_main_forgot_password.html");
31const AUTH_MAIN_RESET_PW_PARTIAL: &str =
32    include_str!("templates/_partials/_auth_main_reset_password.html");
33const AUTH_MAIN_MFA_CHALLENGE_PARTIAL: &str =
34    include_str!("templates/_partials/_auth_main_mfa_challenge.html");
35const AUTH_MAIN_MFA_SETUP_PARTIAL: &str =
36    include_str!("templates/_partials/_auth_main_mfa_setup.html");
37const AUTH_MAIN_MFA_RECOVERY_PARTIAL: &str =
38    include_str!("templates/_partials/_auth_main_mfa_recovery.html");
39const AUTH_MAIN_CONSENT_PARTIAL: &str = include_str!("templates/_partials/_auth_main_consent.html");
40const ERROR_HTML: &str = include_str!("templates/error.html");
41
42/// Register the default browser templates into an existing environment.
43///
44/// Useful for consumers (like the standalone binary) that need to extend
45/// the default template set with additional templates of their own.
46///
47/// # Integrator-overridable blocks (auth shell)
48///
49/// `_partials/_auth_shell.html` exposes two named blocks that integrators
50/// can override from a child template without forking the shell:
51///
52/// - `splash_content` — replaces the splash aside's body (left column).
53///   Default includes `_partials/_splash.html`, which renders a shader
54///   canvas (or sandboxed iframe when `branding.splash_url` is set).
55/// - `auth_main` — replaces the entire `<main class="wf-auth-form">`
56///   subtree. During the z3c migration (C3–C10) the default body of this
57///   block contains a transitional bridge that re-exposes
58///   `{% block auth_top %}` and `{% block form %}` sub-blocks so
59///   un-migrated pages keep working. Once all pages have migrated to their
60///   `_auth_main_<page>.html` partials, the bridge and its sub-blocks will
61///   be removed and `auth_main` becomes the sole integrator entry point.
62///
63/// Both blocks are safe to override in integrator templates that
64/// `{% extends "_partials/_auth_shell.html" %}` — the surrounding
65/// `<aside class="wf-auth-splash">` wrapper and the auth_main slot are
66/// owned by the shell and remain stable.
67///
68/// # Integrator-overridable blocks (app shell)
69///
70/// `_partials/_app_shell.html` exposes six named blocks on the
71/// post-auth surface for pageheader / panel / layout customisation.
72/// Each default is empty (or a safe passthrough); built-in admin and
73/// settings pages override them as appropriate:
74///
75/// - `pagetitle` — page title inside `<h1 class="wf-pagetitle">`.
76///   Default: empty.
77/// - `crumbs` — breadcrumb line inside `<div class="wf-crumbs">`.
78///   Default: empty.
79/// - `page_meta` — right-aligned status cluster inside
80///   `<div class="wf-page-meta">` within `.wf-pageheader`. Default: empty.
81/// - `topbar` — row above the pageheader, typically a search or
82///   command-K bar inside `.wf-topbar`. Default: empty.
83/// - `main_class` — modifier class on `<div class="wf-main">`.
84///   Default: `has-header`. List pages override to `has-tablewrap` so
85///   the grid makes room for a `.wf-tablewrap` region below the header.
86/// - `page_content` — replaces the `.wf-scroll > {% block content %}`
87///   body wholesale. Default: passthrough that renders `{% block content %}`
88///   unchanged, so templates predating the pageheader chrome keep working.
89///
90/// All six blocks are safe to override from any child template that
91/// `{% extends "_partials/_app_shell.html" %}`. The surrounding
92/// `.wf-shell` / `.wf-sidebar` / `.wf-main` structure is owned by the
93/// shell and remains stable.
94pub fn add_default_browser_templates(env: &mut Environment<'static>) {
95    env.add_filter("datefmt", |value: String| -> String {
96        if value.len() >= 16 {
97            let date = &value[..10];
98            let time = &value[11..16];
99            format!("{date} {time} UTC")
100        } else {
101            value
102        }
103    });
104
105    env.add_template_owned("base.html", BASE_HTML)
106        .expect("base.html");
107    env.add_template_owned("login.html", LOGIN_HTML)
108        .expect("login.html");
109    env.add_template_owned("register.html", REGISTER_HTML)
110        .expect("register.html");
111    env.add_template_owned("settings.html", SETTINGS_HTML)
112        .expect("settings.html");
113    env.add_template_owned("consent.html", CONSENT_HTML)
114        .expect("consent.html");
115    env.add_template_owned("forgot_password.html", FORGOT_PASSWORD_HTML)
116        .expect("forgot_password.html");
117    env.add_template_owned("reset_password.html", RESET_PASSWORD_HTML)
118        .expect("reset_password.html");
119    env.add_template_owned("mfa_setup.html", MFA_SETUP_HTML)
120        .expect("mfa_setup.html");
121    env.add_template_owned("mfa_recovery.html", MFA_RECOVERY_HTML)
122        .expect("mfa_recovery.html");
123    env.add_template_owned("mfa_challenge.html", MFA_CHALLENGE_HTML)
124        .expect("mfa_challenge.html");
125    env.add_template_owned("_partials/_modeline.html", MODELINE_PARTIAL)
126        .expect("_partials/_modeline.html");
127    env.add_template_owned("_partials/_flash.html", FLASH_PARTIAL)
128        .expect("_partials/_flash.html");
129    env.add_template_owned("_partials/_splash.html", SPLASH_PARTIAL)
130        .expect("_partials/_splash.html");
131    env.add_template_owned("_partials/_auth_shell.html", AUTH_SHELL_PARTIAL)
132        .expect("_partials/_auth_shell.html");
133    env.add_template_owned("_partials/_app_shell.html", APP_SHELL_PARTIAL)
134        .expect("_partials/_app_shell.html");
135    env.add_template_owned("_partials/_sidebar_nav.html", SIDEBAR_NAV_PARTIAL)
136        .expect("_partials/_sidebar_nav.html");
137    env.add_template_owned("_partials/_auth_macros.html", AUTH_MACROS_PARTIAL)
138        .expect("_partials/_auth_macros.html");
139    env.add_template_owned("_partials/_auth_oob_head.html", AUTH_OOB_HEAD_PARTIAL)
140        .expect("_partials/_auth_oob_head.html");
141    env.add_template_owned("_partials/_auth_main_login.html", AUTH_MAIN_LOGIN_PARTIAL)
142        .expect("_partials/_auth_main_login.html");
143    env.add_template_owned(
144        "_partials/_auth_main_register.html",
145        AUTH_MAIN_REGISTER_PARTIAL,
146    )
147    .expect("_partials/_auth_main_register.html");
148    env.add_template_owned(
149        "_partials/_auth_main_forgot_password.html",
150        AUTH_MAIN_FORGOT_PW_PARTIAL,
151    )
152    .expect("_partials/_auth_main_forgot_password.html");
153    env.add_template_owned(
154        "_partials/_auth_main_reset_password.html",
155        AUTH_MAIN_RESET_PW_PARTIAL,
156    )
157    .expect("_partials/_auth_main_reset_password.html");
158    env.add_template_owned(
159        "_partials/_auth_main_mfa_challenge.html",
160        AUTH_MAIN_MFA_CHALLENGE_PARTIAL,
161    )
162    .expect("_partials/_auth_main_mfa_challenge.html");
163    env.add_template_owned(
164        "_partials/_auth_main_mfa_setup.html",
165        AUTH_MAIN_MFA_SETUP_PARTIAL,
166    )
167    .expect("_partials/_auth_main_mfa_setup.html");
168    env.add_template_owned(
169        "_partials/_auth_main_mfa_recovery.html",
170        AUTH_MAIN_MFA_RECOVERY_PARTIAL,
171    )
172    .expect("_partials/_auth_main_mfa_recovery.html");
173    env.add_template_owned(
174        "_partials/_auth_main_consent.html",
175        AUTH_MAIN_CONSENT_PARTIAL,
176    )
177    .expect("_partials/_auth_main_consent.html");
178    env.add_template_owned("error.html", ERROR_HTML)
179        .expect("error.html");
180}
181
182pub fn build_default_browser_env() -> Arc<Environment<'static>> {
183    let mut env = Environment::new();
184    add_default_browser_templates(&mut env);
185    Arc::new(env)
186}
187
188pub fn render(
189    env: &Environment<'_>,
190    template_name: &str,
191    ctx: minijinja::value::Value,
192) -> Result<Html<String>, BrowserError> {
193    let tmpl = env.get_template(template_name)?;
194    let rendered = tmpl.render(ctx)?;
195    Ok(Html(rendered))
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn default_env_loads_all_browser_templates() {
204        let env = build_default_browser_env();
205        for name in [
206            "base.html",
207            "login.html",
208            "register.html",
209            "settings.html",
210            "consent.html",
211            "forgot_password.html",
212            "reset_password.html",
213            "mfa_setup.html",
214            "mfa_recovery.html",
215            "mfa_challenge.html",
216            "_partials/_modeline.html",
217            "_partials/_flash.html",
218            "_partials/_splash.html",
219            "_partials/_auth_shell.html",
220            "_partials/_app_shell.html",
221            "_partials/_sidebar_nav.html",
222            "_partials/_auth_macros.html",
223            "_partials/_auth_oob_head.html",
224            "_partials/_auth_main_login.html",
225            "_partials/_auth_main_register.html",
226            "_partials/_auth_main_forgot_password.html",
227            "_partials/_auth_main_reset_password.html",
228            "_partials/_auth_main_mfa_challenge.html",
229            "_partials/_auth_main_mfa_setup.html",
230            "_partials/_auth_main_mfa_recovery.html",
231            "_partials/_auth_main_consent.html",
232            "error.html",
233        ] {
234            assert!(
235                env.get_template(name).is_ok(),
236                "template {name} should be loadable"
237            );
238        }
239    }
240
241    #[test]
242    fn render_produces_html() {
243        let env = build_default_browser_env();
244        let result = render(
245            &env,
246            "login.html",
247            minijinja::context! {
248                csrf_token => "test",
249                is_production => false,
250            },
251        );
252        assert!(result.is_ok());
253    }
254}