adminx/helpers/
template_helper.rs

1// adminx/src/helpers/template_helper.rs
2use 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
13// Centralized template list to keep code clean and DRY
14const 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![]); // Disable autoescaping if rendering raw HTML
38    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
65// Template rendering with authentication context
66pub async fn render_template_with_auth(
67    template_name: &str,
68    mut context: Context,
69    session: &Session,
70    config: &AdminxConfig,
71) -> HttpResponse {
72    // Add authentication context to templates
73    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
90// Protected template rendering (redirects if not authenticated)
91pub 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
117// Template rendering with role-based access control
118pub 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            // Check if user has any of the required roles
129            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
160// Error page renderers
161pub 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
198// Template context helpers
199pub 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    // messages is Vec<(level, message)> where level is "success", "error", "warning", "info"
209    context.insert("flash_messages", &messages);
210    context
211}