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 ("login.html", include_str!("templates/login.html.tera")),
54 ("mfa_setup.html", include_str!("templates/mfa_setup.html.tera")),
55 ("mfa_backup.html", include_str!("templates/mfa_backup.html.tera")),
56 ("mfa_verify.html", include_str!("templates/mfa_verify.html.tera")),
57 ])
58 .expect("adminx: failed to parse embedded templates");
59 tera.autoescape_on(vec![".html"]);
60 tera
61 };
62}
63
64pub fn render(name: &str, ctx: &Context) -> ApiResponse {
66 match TEMPLATES.render(name, ctx) {
67 Ok(html) => ApiResponse::html(200, html),
68 Err(e) => {
69 tracing::error!("adminx template render error [{name}]: {e}");
70 ApiResponse::error(CoreError::Internal(format!("template error: {e}")))
71 }
72 }
73}
74
75pub fn render_with_csrf(ctx: &ReqCtx, mut context: Context, template: &str) -> ApiResponse {
80 let (token, cookie) = crate::csrf::ensure(ctx);
81 context.insert("csrf_token", &token);
82 let resp = render(template, &context);
83 match cookie {
84 Some(v) => resp.with_header("Set-Cookie", v),
85 None => resp,
86 }
87}
88
89pub fn base_context(ctx: &ReqCtx, title: &str) -> Context {
91 let mut c = Context::new();
92 c.insert("title", title);
93 c.insert("mount", &ctx.mount);
94 c.insert("menus", &get_registered_menus());
95 c.insert("is_authenticated", &ctx.claims.is_some());
96 c.insert("tailwind_src", &tailwind_src());
97 c
98}
99
100pub fn dashboard(ctx: &ReqCtx) -> ApiResponse {
102 let c = base_context(ctx, "Dashboard");
103 render("dashboard.html", &c)
104}
105
106pub fn derive_headers(rows: &[Value], pk: &str) -> Vec<String> {
109 let mut headers: Vec<String> = Vec::new();
110 if let Some(Value::Object(first)) = rows.first() {
111 if first.contains_key(pk) {
112 headers.push(pk.to_string());
113 }
114 for k in first.keys() {
115 if k != pk {
116 headers.push(k.clone());
117 }
118 }
119 }
120 headers
121}
122
123pub fn default_fields(columns: &[&str]) -> Vec<Value> {
126 columns
127 .iter()
128 .map(|name| {
129 let field_type = if *name == "deleted" { "checkbox" } else { "text" };
130 json!({
131 "name": name,
132 "label": humanize(name),
133 "field_type": field_type,
134 })
135 })
136 .collect()
137}
138
139pub fn fields_from_structure(structure: &Value) -> Vec<Value> {
142 if let Some(groups) = structure.get("groups").and_then(|g| g.as_array()) {
143 return groups
144 .iter()
145 .filter_map(|g| g.get("fields").and_then(|f| f.as_array()))
146 .flatten()
147 .cloned()
148 .collect();
149 }
150 if let Some(fields) = structure.get("fields").and_then(|f| f.as_array()) {
151 return fields.clone();
152 }
153 Vec::new()
154}
155
156pub fn form_to_json(form: HashMap<String, String>) -> Value {
160 let mut map = Map::new();
161 for (k, v) in form {
162 let value = if v == "true" {
163 Value::Bool(true)
164 } else if v == "false" {
165 Value::Bool(false)
166 } else if let Ok(i) = v.parse::<i64>() {
167 Value::from(i)
168 } else if let Ok(f) = v.parse::<f64>() {
169 Value::from(f)
170 } else {
171 Value::String(v)
172 };
173 map.insert(k, value);
174 }
175 Value::Object(map)
176}
177
178pub fn humanize(name: &str) -> String {
180 name.split('_')
181 .map(|w| {
182 let mut chars = w.chars();
183 match chars.next() {
184 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
185 None => String::new(),
186 }
187 })
188 .collect::<Vec<_>>()
189 .join(" ")
190}