1use actix_web::{HttpResponse};
3use actix_session::Session;
4use once_cell::sync::Lazy;
5use std::sync::Arc;
6use tera::{Context, Tera};
7use crate::configs::initializer::AdminxConfig;
8use crate::utils::auth::extract_claims_from_session;
9use tracing::{error, warn};
10use chrono::Datelike;
11
12
13const TEMPLATE_FILES: &[(&str, &str)] = &[
15 ("layout.html.tera", include_str!("../templates/layout.html.tera")),
16 ("header.html.tera", include_str!("../templates/header.html.tera")),
17 ("footer.html.tera", include_str!("../templates/footer.html.tera")),
18 ("list.html.tera", include_str!("../templates/list.html.tera")),
19 ("new.html.tera", include_str!("../templates/new.html.tera")),
20 ("edit.html.tera", include_str!("../templates/edit.html.tera")),
21 ("view.html.tera", include_str!("../templates/view.html.tera")),
22 ("login.html.tera", include_str!("../templates/login.html.tera")),
23 ("profile.html.tera", include_str!("../templates/profile.html.tera")),
24 ("stats.html.tera", include_str!("../templates/stats.html.tera")),
25 ("errors/404.html.tera", include_str!("../templates/errors/404.html.tera")),
26 ("errors/500.html.tera", include_str!("../templates/errors/500.html.tera")),
27];
28
29pub static ADMINX_TEMPLATES: Lazy<Arc<Tera>> = Lazy::new(|| {
30 let mut tera = Tera::default();
31
32 for (name, content) in TEMPLATE_FILES {
33 tera.add_raw_template(name, content)
34 .unwrap_or_else(|e| panic!("Failed to add {}: {}", name, e));
35 }
36
37 tera.autoescape_on(vec![]); Arc::new(tera)
39});
40
41pub async fn render_template(template_name: &str, ctx: Context) -> HttpResponse {
42 let tera = Arc::clone(&ADMINX_TEMPLATES);
43 match tera.render(template_name, &ctx) {
44 Ok(html) => HttpResponse::Ok().content_type("text/html").body(html),
45 Err(err) => {
46 error!("Template render error for {}: {:?}", template_name, err);
47 let mut error_ctx = Context::new();
48 error_ctx.insert("error", &err.to_string());
49 error_ctx.insert("template_name", template_name);
50
51 let fallback_html = tera
52 .render("errors/500.html.tera", &error_ctx)
53 .unwrap_or_else(|_| format!(
54 "<h1>Internal Server Error</h1><p>Failed to render template: {}</p><p>Error: {}</p>",
55 template_name,
56 err
57 ));
58 HttpResponse::InternalServerError()
59 .content_type("text/html")
60 .body(fallback_html)
61 }
62 }
63}
64
65pub async fn render_template_with_auth(
67 template_name: &str,
68 mut context: Context,
69 session: &Session,
70 config: &AdminxConfig,
71) -> HttpResponse {
72 match extract_claims_from_session(session, config).await {
74 Ok(claims) => {
75 context.insert("current_user", &claims);
76 context.insert("is_authenticated", &true);
77 context.insert("user_email", &claims.email);
78 context.insert("user_role", &claims.role);
79 context.insert("user_roles", &claims.roles);
80 }
81 Err(_) => {
82 context.insert("is_authenticated", &false);
83 context.insert("current_user", &serde_json::Value::Null);
84 }
85 }
86
87 render_template(template_name, context).await
88}
89
90pub async fn render_protected_template(
92 template_name: &str,
93 mut context: Context,
94 session: &Session,
95 config: &AdminxConfig,
96 redirect_url: Option<&str>,
97) -> HttpResponse {
98 match extract_claims_from_session(session, config).await {
99 Ok(claims) => {
100 context.insert("current_user", &claims);
101 context.insert("is_authenticated", &true);
102 context.insert("user_email", &claims.email);
103 context.insert("user_role", &claims.role);
104 context.insert("user_roles", &claims.roles);
105
106 render_template(template_name, context).await
107 }
108 Err(_) => {
109 let redirect_to = redirect_url.unwrap_or("/adminx/login");
110 HttpResponse::Found()
111 .append_header(("Location", redirect_to))
112 .finish()
113 }
114 }
115}
116
117pub async fn render_role_protected_template(
119 template_name: &str,
120 mut context: Context,
121 session: &Session,
122 config: &AdminxConfig,
123 required_roles: Vec<&str>,
124 redirect_url: Option<&str>,
125) -> HttpResponse {
126 match extract_claims_from_session(session, config).await {
127 Ok(claims) => {
128 let user_roles: std::collections::HashSet<String> = {
130 let mut roles = claims.roles.clone();
131 roles.push(claims.role.clone());
132 roles.into_iter().collect()
133 };
134
135 let has_required_role = required_roles.iter()
136 .any(|role| user_roles.contains(&role.to_string()));
137
138 if has_required_role {
139 context.insert("current_user", &claims);
140 context.insert("is_authenticated", &true);
141 context.insert("user_email", &claims.email);
142 context.insert("user_role", &claims.role);
143 context.insert("user_roles", &claims.roles);
144
145 render_template(template_name, context).await
146 } else {
147 warn!("Access denied for user {} to template {}", claims.email, template_name);
148 render_403().await
149 }
150 }
151 Err(_) => {
152 let redirect_to = redirect_url.unwrap_or("/adminx/login");
153 HttpResponse::Found()
154 .append_header(("Location", redirect_to))
155 .finish()
156 }
157 }
158}
159
160pub async fn render_404() -> HttpResponse {
162 let tera = Arc::clone(&ADMINX_TEMPLATES);
163 let ctx = Context::new();
164 let html = tera
165 .render("errors/404.html.tera", &ctx)
166 .unwrap_or_else(|_| "<h1>404 - Page Not Found</h1>".to_string());
167 HttpResponse::NotFound()
168 .content_type("text/html")
169 .body(html)
170}
171
172pub async fn render_403() -> HttpResponse {
173 let tera = Arc::clone(&ADMINX_TEMPLATES);
174 let mut ctx = Context::new();
175 ctx.insert("error_message", "You don't have permission to access this resource.");
176
177 let html = tera
178 .render("errors/403.html.tera", &ctx)
179 .unwrap_or_else(|_| "<h1>403 - Access Forbidden</h1><p>You don't have permission to access this resource.</p>".to_string());
180 HttpResponse::Forbidden()
181 .content_type("text/html")
182 .body(html)
183}
184
185pub async fn render_500(error_message: Option<&str>) -> HttpResponse {
186 let tera = Arc::clone(&ADMINX_TEMPLATES);
187 let mut ctx = Context::new();
188 ctx.insert("error_message", &error_message.unwrap_or("An internal server error occurred."));
189
190 let html = tera
191 .render("errors/500.html.tera", &ctx)
192 .unwrap_or_else(|_| "<h1>500 - Internal Server Error</h1>".to_string());
193 HttpResponse::InternalServerError()
194 .content_type("text/html")
195 .body(html)
196}
197
198pub fn create_base_context() -> Context {
200 let mut ctx = Context::new();
201 ctx.insert("app_name", "AdminX");
202 ctx.insert("app_version", env!("CARGO_PKG_VERSION"));
203 ctx.insert("current_year", &chrono::Utc::now().year());
204 ctx
205}
206
207pub fn add_flash_messages(mut context: Context, messages: Vec<(&str, &str)>) -> Context {
208 context.insert("flash_messages", &messages);
210 context
211}