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 =
40    include_str!("templates/_partials/_auth_main_consent.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_template_owned("base.html", BASE_HTML)
96        .expect("base.html");
97    env.add_template_owned("login.html", LOGIN_HTML)
98        .expect("login.html");
99    env.add_template_owned("register.html", REGISTER_HTML)
100        .expect("register.html");
101    env.add_template_owned("settings.html", SETTINGS_HTML)
102        .expect("settings.html");
103    env.add_template_owned("consent.html", CONSENT_HTML)
104        .expect("consent.html");
105    env.add_template_owned("forgot_password.html", FORGOT_PASSWORD_HTML)
106        .expect("forgot_password.html");
107    env.add_template_owned("reset_password.html", RESET_PASSWORD_HTML)
108        .expect("reset_password.html");
109    env.add_template_owned("mfa_setup.html", MFA_SETUP_HTML)
110        .expect("mfa_setup.html");
111    env.add_template_owned("mfa_recovery.html", MFA_RECOVERY_HTML)
112        .expect("mfa_recovery.html");
113    env.add_template_owned("mfa_challenge.html", MFA_CHALLENGE_HTML)
114        .expect("mfa_challenge.html");
115    env.add_template_owned("_partials/_modeline.html", MODELINE_PARTIAL)
116        .expect("_partials/_modeline.html");
117    env.add_template_owned("_partials/_flash.html", FLASH_PARTIAL)
118        .expect("_partials/_flash.html");
119    env.add_template_owned("_partials/_splash.html", SPLASH_PARTIAL)
120        .expect("_partials/_splash.html");
121    env.add_template_owned("_partials/_auth_shell.html", AUTH_SHELL_PARTIAL)
122        .expect("_partials/_auth_shell.html");
123    env.add_template_owned("_partials/_app_shell.html", APP_SHELL_PARTIAL)
124        .expect("_partials/_app_shell.html");
125    env.add_template_owned("_partials/_sidebar_nav.html", SIDEBAR_NAV_PARTIAL)
126        .expect("_partials/_sidebar_nav.html");
127    env.add_template_owned("_partials/_auth_macros.html", AUTH_MACROS_PARTIAL)
128        .expect("_partials/_auth_macros.html");
129    env.add_template_owned("_partials/_auth_oob_head.html", AUTH_OOB_HEAD_PARTIAL)
130        .expect("_partials/_auth_oob_head.html");
131    env.add_template_owned("_partials/_auth_main_login.html", AUTH_MAIN_LOGIN_PARTIAL)
132        .expect("_partials/_auth_main_login.html");
133    env.add_template_owned(
134        "_partials/_auth_main_register.html",
135        AUTH_MAIN_REGISTER_PARTIAL,
136    )
137    .expect("_partials/_auth_main_register.html");
138    env.add_template_owned(
139        "_partials/_auth_main_forgot_password.html",
140        AUTH_MAIN_FORGOT_PW_PARTIAL,
141    )
142    .expect("_partials/_auth_main_forgot_password.html");
143    env.add_template_owned(
144        "_partials/_auth_main_reset_password.html",
145        AUTH_MAIN_RESET_PW_PARTIAL,
146    )
147    .expect("_partials/_auth_main_reset_password.html");
148    env.add_template_owned(
149        "_partials/_auth_main_mfa_challenge.html",
150        AUTH_MAIN_MFA_CHALLENGE_PARTIAL,
151    )
152    .expect("_partials/_auth_main_mfa_challenge.html");
153    env.add_template_owned(
154        "_partials/_auth_main_mfa_setup.html",
155        AUTH_MAIN_MFA_SETUP_PARTIAL,
156    )
157    .expect("_partials/_auth_main_mfa_setup.html");
158    env.add_template_owned(
159        "_partials/_auth_main_mfa_recovery.html",
160        AUTH_MAIN_MFA_RECOVERY_PARTIAL,
161    )
162    .expect("_partials/_auth_main_mfa_recovery.html");
163    env.add_template_owned(
164        "_partials/_auth_main_consent.html",
165        AUTH_MAIN_CONSENT_PARTIAL,
166    )
167    .expect("_partials/_auth_main_consent.html");
168}
169
170pub fn build_default_browser_env() -> Arc<Environment<'static>> {
171    let mut env = Environment::new();
172    add_default_browser_templates(&mut env);
173    Arc::new(env)
174}
175
176pub fn render(
177    env: &Environment<'_>,
178    template_name: &str,
179    ctx: minijinja::value::Value,
180) -> Result<Html<String>, BrowserError> {
181    let tmpl = env.get_template(template_name)?;
182    let rendered = tmpl.render(ctx)?;
183    Ok(Html(rendered))
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn default_env_loads_all_browser_templates() {
192        let env = build_default_browser_env();
193        for name in [
194            "base.html",
195            "login.html",
196            "register.html",
197            "settings.html",
198            "consent.html",
199            "forgot_password.html",
200            "reset_password.html",
201            "mfa_setup.html",
202            "mfa_recovery.html",
203            "mfa_challenge.html",
204            "_partials/_modeline.html",
205            "_partials/_flash.html",
206            "_partials/_splash.html",
207            "_partials/_auth_shell.html",
208            "_partials/_app_shell.html",
209            "_partials/_sidebar_nav.html",
210            "_partials/_auth_macros.html",
211            "_partials/_auth_oob_head.html",
212            "_partials/_auth_main_login.html",
213            "_partials/_auth_main_register.html",
214            "_partials/_auth_main_forgot_password.html",
215            "_partials/_auth_main_reset_password.html",
216            "_partials/_auth_main_mfa_challenge.html",
217            "_partials/_auth_main_mfa_setup.html",
218            "_partials/_auth_main_mfa_recovery.html",
219            "_partials/_auth_main_consent.html",
220        ] {
221            assert!(
222                env.get_template(name).is_ok(),
223                "template {name} should be loadable"
224            );
225        }
226    }
227
228    #[test]
229    fn render_produces_html() {
230        let env = build_default_browser_env();
231        let result = render(
232            &env,
233            "login.html",
234            minijinja::context! {
235                csrf_token => "test",
236                is_production => false,
237            },
238        );
239        assert!(result.is_ok());
240    }
241}