Skip to main content

rustio_admin_assets/
lib.rs

1//! Embedded admin templates for `rustio-admin`.
2//!
3//! This crate is the single owner of the admin panel's template bytes.
4//! They are baked into the binary via `include_str!` at compile time —
5//! single-binary deploy is a hard constraint of the framework.
6//!
7//! Two crates consume this:
8//!
9//! * `rustio-admin` (the runtime) reads [`EMBEDDED_TEMPLATES`] to build
10//!   its minijinja loader and to validate project overrides.
11//! * `rustio-admin-cli` reads [`embedded_template_names`] /
12//!   [`embedded_template_source`] for the `rustio-admin override`
13//!   command, which materialises a copy of a framework template on disk.
14//!
15//! The crate is deliberately std-only and dependency-free. It carries a
16//! low MSRV so the lightweight parts of the CLI can depend on it without
17//! pulling in — or inheriting the Rust version of — the runtime stack
18//! (`sqlx`, `tokio`, `hyper`). A disk-side override still wins at render
19//! time; that lookup lives in the runtime, not here. This crate only
20//! owns the *defaults*.
21
22/// Every template baked into the framework binary, by canonical name
23/// (e.g. `"admin/list.html"`). Order is stable across builds so a CLI
24/// verb that lists them produces deterministic output. The runtime
25/// iterates this to build its loader and to validate project overrides;
26/// [`embedded_template_names`] and [`embedded_template_source`] are the
27/// name/lookup views over it.
28pub const EMBEDDED_TEMPLATES: &[(&str, &str)] = &[
29    // Shell + partials
30    (
31        "admin/_base.html",
32        include_str!("../assets/templates/admin/_base.html"),
33    ),
34    (
35        "admin/_topbar.html",
36        include_str!("../assets/templates/admin/_topbar.html"),
37    ),
38    (
39        "admin/_sidebar.html",
40        include_str!("../assets/templates/admin/_sidebar.html"),
41    ),
42    (
43        "admin/_theme.html",
44        include_str!("../assets/templates/admin/_theme.html"),
45    ),
46    (
47        "admin/_row_actions.html",
48        include_str!("../assets/templates/admin/_row_actions.html"),
49    ),
50    (
51        "admin/includes/_form_field.html",
52        include_str!("../assets/templates/admin/includes/_form_field.html"),
53    ),
54    (
55        "admin/includes/_field_errors.html",
56        include_str!("../assets/templates/admin/includes/_field_errors.html"),
57    ),
58    // Generic pages
59    (
60        "admin/login.html",
61        include_str!("../assets/templates/admin/login.html"),
62    ),
63    (
64        "admin/index.html",
65        include_str!("../assets/templates/admin/index.html"),
66    ),
67    (
68        "admin/list.html",
69        include_str!("../assets/templates/admin/list.html"),
70    ),
71    (
72        "admin/form.html",
73        include_str!("../assets/templates/admin/form.html"),
74    ),
75    (
76        "admin/confirm_delete.html",
77        include_str!("../assets/templates/admin/confirm_delete.html"),
78    ),
79    (
80        "admin/bulk_confirm_delete.html",
81        include_str!("../assets/templates/admin/bulk_confirm_delete.html"),
82    ),
83    (
84        "admin/db_browser.html",
85        include_str!("../assets/templates/admin/db_browser.html"),
86    ),
87    (
88        "admin/bulk_confirm_action.html",
89        include_str!("../assets/templates/admin/bulk_confirm_action.html"),
90    ),
91    (
92        "admin/error.html",
93        include_str!("../assets/templates/admin/error.html"),
94    ),
95    (
96        "admin/forbidden.html",
97        include_str!("../assets/templates/admin/forbidden.html"),
98    ),
99    // Audit / password change
100    (
101        "admin/object_history.html",
102        include_str!("../assets/templates/admin/object_history.html"),
103    ),
104    (
105        "admin/log_entries.html",
106        include_str!("../assets/templates/admin/log_entries.html"),
107    ),
108    (
109        "admin/apis_index.html",
110        include_str!("../assets/templates/admin/apis_index.html"),
111    ),
112    (
113        "admin/apis_playground.html",
114        include_str!("../assets/templates/admin/apis_playground.html"),
115    ),
116    (
117        "admin/health.html",
118        include_str!("../assets/templates/admin/health.html"),
119    ),
120    (
121        "admin/feature_flags.html",
122        include_str!("../assets/templates/admin/feature_flags.html"),
123    ),
124    (
125        "admin/_list_adaptive.html",
126        include_str!("../assets/templates/admin/_list_adaptive.html"),
127    ),
128    (
129        "admin/view_designer.html",
130        include_str!("../assets/templates/admin/view_designer.html"),
131    ),
132    (
133        "admin/view_designer_model.html",
134        include_str!("../assets/templates/admin/view_designer_model.html"),
135    ),
136    (
137        "admin/branding.html",
138        include_str!("../assets/templates/admin/branding.html"),
139    ),
140    (
141        "admin/schema.html",
142        include_str!("../assets/templates/admin/schema.html"),
143    ),
144    (
145        "admin/view_layer/_cell.html",
146        include_str!("../assets/templates/admin/view_layer/_cell.html"),
147    ),
148    (
149        "admin/view_layer/_row.html",
150        include_str!("../assets/templates/admin/view_layer/_row.html"),
151    ),
152    (
153        "admin/notifications.html",
154        include_str!("../assets/templates/admin/notifications.html"),
155    ),
156    (
157        "admin/csv_import_result.html",
158        include_str!("../assets/templates/admin/csv_import_result.html"),
159    ),
160    (
161        "admin/docs_index.html",
162        include_str!("../assets/templates/admin/docs_index.html"),
163    ),
164    (
165        "admin/doc_page.html",
166        include_str!("../assets/templates/admin/doc_page.html"),
167    ),
168    (
169        "admin/password_change.html",
170        include_str!("../assets/templates/admin/password_change.html"),
171    ),
172    // Built-in user pages
173    (
174        "admin/users_list.html",
175        include_str!("../assets/templates/admin/users_list.html"),
176    ),
177    (
178        "admin/user_new.html",
179        include_str!("../assets/templates/admin/user_new.html"),
180    ),
181    (
182        "admin/user_edit.html",
183        include_str!("../assets/templates/admin/user_edit.html"),
184    ),
185    (
186        "admin/user_view.html",
187        include_str!("../assets/templates/admin/user_view.html"),
188    ),
189    (
190        "admin/user_confirm_delete.html",
191        include_str!("../assets/templates/admin/user_confirm_delete.html"),
192    ),
193    // Built-in group pages
194    (
195        "admin/groups_list.html",
196        include_str!("../assets/templates/admin/groups_list.html"),
197    ),
198    (
199        "admin/group_new.html",
200        include_str!("../assets/templates/admin/group_new.html"),
201    ),
202    (
203        "admin/group_edit.html",
204        include_str!("../assets/templates/admin/group_edit.html"),
205    ),
206    (
207        "admin/group_confirm_delete.html",
208        include_str!("../assets/templates/admin/group_confirm_delete.html"),
209    ),
210    // Self-service account pages (R0+)
211    (
212        "admin/account_sessions.html",
213        include_str!("../assets/templates/admin/account_sessions.html"),
214    ),
215    // Self-service password recovery (R1)
216    (
217        "admin/forgot_password.html",
218        include_str!("../assets/templates/admin/forgot_password.html"),
219    ),
220    (
221        "admin/forgot_password_sent.html",
222        include_str!("../assets/templates/admin/forgot_password_sent.html"),
223    ),
224    (
225        "admin/reset_password.html",
226        include_str!("../assets/templates/admin/reset_password.html"),
227    ),
228    // Organisational recovery (R2)
229    //
230    // These pages are rendered by `admin/admin_recovery_handlers.rs`
231    // — the admin-driven reset / lock / re-auth / forced-rotation
232    // surface. They were inadvertently omitted from this list when
233    // R2 shipped in 0.6.0; the disk files were committed but never
234    // hooked into the embedded set. Without them every
235    // `/admin/reauth`, `/admin/users/:id/reset-password`,
236    // `/admin/users/:id/lock`, and forced-password-change request
237    // returns the framework's generic 500 page. The
238    // `every_handler_rendered_template_resolves` test in
239    // `rustio-admin` is the regression gate that catches this shape
240    // of omission.
241    (
242        "admin/reauth.html",
243        include_str!("../assets/templates/admin/reauth.html"),
244    ),
245    (
246        "admin/admin_reset_password.html",
247        include_str!("../assets/templates/admin/admin_reset_password.html"),
248    ),
249    (
250        "admin/lock_user.html",
251        include_str!("../assets/templates/admin/lock_user.html"),
252    ),
253    (
254        "admin/confirm_admin_action.html",
255        include_str!("../assets/templates/admin/confirm_admin_action.html"),
256    ),
257    (
258        "admin/must_change_password.html",
259        include_str!("../assets/templates/admin/must_change_password.html"),
260    ),
261    // TOTP MFA (R3)
262    //
263    // Rendered by `admin/mfa_handlers.rs` (enrol / verify /
264    // regenerate / disable). Same shape of omission as the R2 set
265    // above; same regression-gate test covers them.
266    (
267        "admin/mfa_enroll.html",
268        include_str!("../assets/templates/admin/mfa_enroll.html"),
269    ),
270    (
271        "admin/mfa_enroll_complete.html",
272        include_str!("../assets/templates/admin/mfa_enroll_complete.html"),
273    ),
274    (
275        "admin/mfa_verify.html",
276        include_str!("../assets/templates/admin/mfa_verify.html"),
277    ),
278    (
279        "admin/mfa_disable.html",
280        include_str!("../assets/templates/admin/mfa_disable.html"),
281    ),
282    (
283        "admin/mfa_regenerate.html",
284        include_str!("../assets/templates/admin/mfa_regenerate.html"),
285    ),
286    (
287        "admin/mfa_regenerate_complete.html",
288        include_str!("../assets/templates/admin/mfa_regenerate_complete.html"),
289    ),
290];
291
292/// Every embedded template name, by canonical path (e.g.
293/// `"admin/list.html"`). Order matches [`EMBEDDED_TEMPLATES`] and is
294/// stable across builds so `rustio-admin override` enumerates copy
295/// candidates deterministically; project code can also iterate this to
296/// build documentation pages.
297pub fn embedded_template_names() -> Vec<&'static str> {
298    EMBEDDED_TEMPLATES.iter().map(|(n, _)| *n).collect()
299}
300
301/// Return the byte-for-byte source of an embedded template by name, or
302/// `None` when no such template exists. Used by `rustio-admin override`
303/// to materialise a copy at `<RUSTIO_TEMPLATE_DIR>/<name>` so the
304/// operator can start editing without first having to find the
305/// framework source.
306pub fn embedded_template_source(name: &str) -> Option<&'static str> {
307    EMBEDDED_TEMPLATES
308        .iter()
309        .find_map(|(n, body)| if *n == name { Some(*body) } else { None })
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    /// The table is non-empty and every entry actually baked in bytes.
317    /// Catches a `git mv` of the asset tree that leaves a dangling
318    /// `include_str!` — that would be a compile error, but this also
319    /// guards against an entry being silently emptied.
320    #[test]
321    fn every_embedded_template_has_bytes() {
322        assert!(!EMBEDDED_TEMPLATES.is_empty());
323        for (name, body) in EMBEDDED_TEMPLATES {
324            assert!(!body.is_empty(), "{name} is empty");
325        }
326    }
327
328    /// The name view and the lookup view agree with the table.
329    #[test]
330    fn accessors_round_trip() {
331        for name in embedded_template_names() {
332            assert!(
333                embedded_template_source(name).is_some(),
334                "{name} listed but not resolvable"
335            );
336        }
337        assert!(embedded_template_source("admin/does-not-exist.html").is_none());
338    }
339}