1use crate::error::CoreError;
8use crate::registry::get_registered_menus;
9use crate::request::ReqCtx;
10use crate::response::ApiResponse;
11use lazy_static::lazy_static;
12use once_cell::sync::OnceCell;
13use serde_json::{json, Map, Value};
14use std::collections::HashMap;
15use tera::{Context, Tera};
16
17const DEFAULT_TAILWIND_SRC: &str = "https://cdn.tailwindcss.com";
21
22static TAILWIND_SRC: OnceCell<String> = OnceCell::new();
23
24pub fn set_tailwind_src(src: impl Into<String>) {
28 let _ = TAILWIND_SRC.set(src.into());
29}
30
31fn tailwind_src() -> String {
33 if let Some(s) = TAILWIND_SRC.get() {
34 return s.clone();
35 }
36 match std::env::var("ADMINX_TAILWIND_SRC") {
37 Ok(s) if !s.is_empty() => s,
38 _ => DEFAULT_TAILWIND_SRC.to_string(),
39 }
40}
41
42lazy_static! {
43 static ref TEMPLATES: Tera = {
44 let mut tera = Tera::default();
45 tera.add_raw_templates(vec![
46 ("layout.html", include_str!("templates/layout.html.tera")),
47 ("header.html", include_str!("templates/header.html.tera")),
48 ("footer.html", include_str!("templates/footer.html.tera")),
49 ("dashboard.html", include_str!("templates/dashboard.html.tera")),
50 ("list.html", include_str!("templates/list.html.tera")),
51 ("form.html", include_str!("templates/form.html.tera")),
52 ("view.html", include_str!("templates/view.html.tera")),
53 ("history.html", include_str!("templates/history.html.tera")),
54 ("login.html", include_str!("templates/login.html.tera")),
55 ("mfa_setup.html", include_str!("templates/mfa_setup.html.tera")),
56 ("mfa_backup.html", include_str!("templates/mfa_backup.html.tera")),
57 ("mfa_verify.html", include_str!("templates/mfa_verify.html.tera")),
58 ])
59 .expect("adminx: failed to parse embedded templates");
60 tera.autoescape_on(vec![".html"]);
61 tera
62 };
63}
64
65pub fn render(name: &str, ctx: &Context) -> ApiResponse {
67 match TEMPLATES.render(name, ctx) {
68 Ok(html) => ApiResponse::html(200, html),
69 Err(e) => {
70 tracing::error!("adminx template render error [{name}]: {e}");
71 ApiResponse::error(CoreError::Internal(format!("template error: {e}")))
72 }
73 }
74}
75
76pub fn render_with_csrf(ctx: &ReqCtx, mut context: Context, template: &str) -> ApiResponse {
81 let (token, cookie) = crate::csrf::ensure(ctx);
82 context.insert("csrf_token", &token);
83 let resp = render(template, &context);
84 match cookie {
85 Some(v) => resp.with_header("Set-Cookie", v),
86 None => resp,
87 }
88}
89
90pub fn base_context(ctx: &ReqCtx, title: &str) -> Context {
92 let mut c = Context::new();
93 c.insert("title", title);
94 c.insert("mount", &ctx.mount);
95 c.insert("menus", &get_registered_menus());
96 c.insert("is_authenticated", &ctx.claims.is_some());
97 c.insert("tailwind_src", &tailwind_src());
98 c
99}
100
101pub fn dashboard(ctx: &ReqCtx) -> ApiResponse {
103 let c = base_context(ctx, "Dashboard");
104 render("dashboard.html", &c)
105}
106
107pub fn derive_headers(rows: &[Value], pk: &str) -> Vec<String> {
110 let mut headers: Vec<String> = Vec::new();
111 if let Some(Value::Object(first)) = rows.first() {
112 if first.contains_key(pk) {
113 headers.push(pk.to_string());
114 }
115 for k in first.keys() {
116 if k != pk {
117 headers.push(k.clone());
118 }
119 }
120 }
121 headers
122}
123
124pub fn default_fields(columns: &[&str]) -> Vec<Value> {
127 columns
128 .iter()
129 .map(|name| {
130 let field_type = if *name == "deleted" { "checkbox" } else { "text" };
131 json!({
132 "name": name,
133 "label": humanize(name),
134 "field_type": field_type,
135 })
136 })
137 .collect()
138}
139
140pub fn fields_from_structure(structure: &Value) -> Vec<Value> {
143 if let Some(groups) = structure.get("groups").and_then(|g| g.as_array()) {
144 return groups
145 .iter()
146 .filter_map(|g| g.get("fields").and_then(|f| f.as_array()))
147 .flatten()
148 .cloned()
149 .collect();
150 }
151 if let Some(fields) = structure.get("fields").and_then(|f| f.as_array()) {
152 return fields.clone();
153 }
154 Vec::new()
155}
156
157pub fn form_to_json(form: HashMap<String, String>) -> Value {
161 let mut map = Map::new();
162 for (k, v) in form {
163 let value = if v == "true" {
164 Value::Bool(true)
165 } else if v == "false" {
166 Value::Bool(false)
167 } else if let Ok(i) = v.parse::<i64>() {
168 Value::from(i)
169 } else if let Ok(f) = v.parse::<f64>() {
170 Value::from(f)
171 } else {
172 Value::String(v)
173 };
174 map.insert(k, value);
175 }
176 Value::Object(map)
177}
178
179pub fn humanize(name: &str) -> String {
181 name.split('_')
182 .map(|w| {
183 let mut chars = w.chars();
184 match chars.next() {
185 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
186 None => String::new(),
187 }
188 })
189 .collect::<Vec<_>>()
190 .join(" ")
191}