Skip to main content

runique/context/
template.rs

1//! Main request context: `AppError`, `RuniqueContext`, and Tera context construction.
2use crate::app::templates::TemplateLoader;
3use crate::auth::session::CurrentUser;
4use crate::errors::error::ErrorContext;
5use crate::flash::Message;
6use crate::forms::{
7    extractor::{Prisme, csrf_required, prisme_pipeline},
8    field::RuniqueForm,
9};
10use crate::impl_from_error;
11use crate::middleware::security::anti_bot::HoneypotFieldName;
12use crate::utils::aliases::{ADb, AEngine, AppResult, StrMap};
13use crate::utils::trad::{t, tf};
14use crate::utils::url_params::UrlParams;
15use crate::utils::{csp_nonce::CspNonce, csrf::CsrfToken};
16use axum::{
17    body::Body,
18    extract::{FromRequest, FromRequestParts, Path},
19    http::{Request as HttpRequest, StatusCode, method::Method},
20    response::{Html, IntoResponse, Response},
21};
22use sea_orm::DbErr;
23use serde::de::DeserializeOwned;
24use std::str::FromStr;
25use std::sync::Arc;
26use tera::Context;
27use tower_sessions::Session;
28use tracing::error;
29
30// --- ERROR HANDLING ---
31
32/// Application error returned by handlers: encapsulates an [`ErrorContext`] and implements [`IntoResponse`].
33pub struct AppError {
34    /// Error context: status code, message, type.
35    pub context: ErrorContext,
36}
37
38impl AppError {
39    /// Wraps an [`ErrorContext`] into an `AppError`.
40    pub fn new(context: ErrorContext) -> Self {
41        Self { context }
42    }
43
44    /// Converts a Tera rendering error into an `AppError`, logging detail in debug mode.
45    pub fn map_tera(e: tera::Error, route: &str, tera: &tera::Tera) -> Box<Self> {
46        // Log the detailed error in the console
47        error!(
48            template = route,
49            error = ?e,
50            "Template rendering error"
51        );
52
53        Box::new(Self {
54            context: ErrorContext::from_tera_error(&e, route, tera),
55        })
56    }
57}
58
59impl_from_error!(anyhow::Error => from_anyhow, DbErr => database);
60
61impl IntoResponse for AppError {
62    fn into_response(self) -> Response {
63        let status = StatusCode::from_u16(self.context.status_code)
64            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
65
66        //  Log the error in detail
67        error!(
68            status = status.as_u16(),
69            error_type = ?self.context.error_type,
70            message = %self.context.message,
71            "AppError occurred"
72        );
73
74        let mut res = status.into_response();
75        //  Insert ErrorContext so that the middleware can retrieve it
76        res.extensions_mut().insert(Arc::new(self.context));
77        res
78    }
79}
80
81impl IntoResponse for Box<AppError> {
82    fn into_response(self) -> Response {
83        (*self).into_response()
84    }
85}
86
87// --- TEMPLATE CONTEXT ---
88
89/// Request context automatically extracted in handlers via `FromRequest`.
90/// Contains the engine, session, flash messages, CSRF token, Tera context,
91/// and the Prisme-extracted form data (query params on GET, body on POST).
92#[derive(Clone)]
93pub struct Request {
94    /// Shared application engine.
95    pub engine: AEngine,
96    /// Session of the current request.
97    pub session: Session,
98    /// Session flash messages.
99    pub notices: Message,
100    /// Request CSRF token (masked in the Tera context).
101    pub csrf_token: CsrfToken,
102    /// Pre-filled Tera context (csrf_token, debug, messages, user…).
103    pub context: Context,
104    /// HTTP method of the request.
105    pub method: Method,
106    /// HTTP headers of the request.
107    pub headers: axum::http::HeaderMap,
108    /// Path parameters (`/{id}`).
109    pub path_params: StrMap,
110    /// Raw query string (`?a=1&b=2`), preserved for typed deserialization.
111    pub raw_query: String,
112    /// Query string parameters.
113    pub query_params: StrMap,
114    /// Current user (None if not authenticated).
115    pub user: Option<CurrentUser>,
116    /// Parsed form data from the request (query params on GET, body on POST).
117    /// CSRF is validated — csrf_valid = false signals an invalid token.
118    pub prisme: Prisme,
119    /// Honeypot field name injected by anti_bot middleware (None if middleware not active).
120    pub honeypot_field_name: Option<String>,
121}
122
123impl<S> FromRequest<S> for Request
124where
125    S: Send + Sync,
126{
127    type Rejection = Response;
128
129    async fn from_request(req: HttpRequest<Body>, state: &S) -> Result<Self, Self::Rejection> {
130        let err = |msg: &str| (StatusCode::INTERNAL_SERVER_ERROR, msg.to_string()).into_response();
131
132        let (mut parts, body) = req.into_parts();
133        let ex = &parts.extensions;
134
135        let engine = ex
136            .get::<AEngine>()
137            .cloned()
138            .ok_or_else(|| err("engine missing"))?;
139        let csrf_token = ex
140            .get::<CsrfToken>()
141            .cloned()
142            .ok_or_else(|| err("csrf missing"))?;
143        let session = ex
144            .get::<Session>()
145            .cloned()
146            .ok_or_else(|| err("session missing"))?;
147        let nonce = ex.get::<CspNonce>().map(|n| n.as_str()).unwrap_or_default();
148        let user = ex.get::<CurrentUser>().cloned();
149        let honeypot_field_name = ex.get::<HoneypotFieldName>().map(|h| h.0.clone());
150
151        let notices = Message {
152            session: session.clone(),
153        };
154        let messages = notices.get_all().await;
155
156        let mut context = Context::new();
157        context.insert("debug", &engine.config.debug);
158        context.insert(
159            "csrf_token",
160            &csrf_token
161                .masked()
162                .unwrap_or_else(|_| csrf_token.clone())
163                .as_str(),
164        );
165        context.insert("csp_nonce", nonce);
166        context.insert("static_runique", &engine.config.static_files);
167        context.insert("messages", &messages);
168        if let Some(ref u) = user {
169            context.insert("current_user", u);
170        }
171
172        let path_params = Path::<StrMap>::from_request_parts(&mut parts, state)
173            .await
174            .map(|Path(p)| p)
175            .unwrap_or_default();
176
177        let ico_image = std::env::var("ICON_IMAGE")
178            .unwrap_or("/runique/static/favicon_runique.ico".to_string());
179        let ico_image =
180            crate::utils::resolve_og_image(&engine.security_hosts, engine.config.debug, &ico_image);
181        context.insert("icon_image", &ico_image);
182
183        let og_image =
184            std::env::var("OG_IMAGE").unwrap_or("/runique/static/runique_320.avif".to_string());
185        let og_image =
186            crate::utils::resolve_og_image(&engine.security_hosts, engine.config.debug, &og_image);
187        context.insert("og_image", &og_image);
188
189        context.insert("current_path", parts.uri.path());
190
191        let raw_query = parts.uri.query().unwrap_or_default().to_string();
192        let query_params = serde_urlencoded::from_str::<StrMap>(&raw_query).unwrap_or_default();
193
194        let method = parts.method.clone();
195        let headers = parts.headers.clone();
196
197        // Run Prisme pipeline (sentinel → aegis → CSRF check)
198        let req = HttpRequest::from_parts(parts, body);
199        let prisme = prisme_pipeline(req, state).await?;
200
201        Ok(Self {
202            engine,
203            session,
204            notices,
205            csrf_token,
206            context,
207            method,
208            headers,
209            path_params,
210            raw_query,
211            query_params,
212            user,
213            prisme,
214            honeypot_field_name,
215        })
216    }
217}
218
219impl Request {
220    /// Builds a bare `Request` outside of the normal `FromRequest` extraction path
221    /// (e.g. for tests or internally-constructed requests), with empty path/query
222    /// params, no user, and `prisme.csrf_valid` fixed to `false` since no body was
223    /// ever parsed to validate a CSRF token against.
224    pub fn new(engine: AEngine, session: Session, csrf_token: CsrfToken, method: Method) -> Self {
225        let mut context = tera::Context::new();
226        // mod reload for templates in debug mode
227        // The backend cannot be reloaded here because it is shared between requests
228        context.insert("debug", &engine.config.debug);
229        context.insert("static_runique", &engine.config.static_files);
230        context.insert(
231            "csrf_token",
232            &csrf_token
233                .masked()
234                .unwrap_or_else(|_| csrf_token.clone())
235                .as_str(),
236        );
237
238        Self {
239            engine,
240            session: session.clone(),
241            notices: Message { session },
242            csrf_token,
243            context,
244            method,
245            headers: axum::http::HeaderMap::new(),
246            path_params: StrMap::new(),
247            raw_query: String::new(),
248            query_params: StrMap::new(),
249            user: None,
250            prisme: Prisme {
251                data: Default::default(),
252                // RuniqueContext (FromRequestParts) never runs check_csrf — no body access.
253                // Fail-closed: only `Request` via `request.form()` can actually validate CSRF.
254                csrf_valid: false,
255            },
256            honeypot_field_name: None,
257        }
258    }
259
260    /// Unique generic rendering to avoid duplication
261    pub fn render(&mut self, template: &str) -> AppResult<Response> {
262        let html_result = if self.engine.config.debug {
263            // In debug mode, Tera is fully reinitialized with the Loader
264            // This applies Regex on {% messages %}, {% form.xxx %}, etc.
265            match TemplateLoader::init(&self.engine.config, self.engine.url_registry.clone()) {
266                Ok(dev_tera) => {
267                    let res = dev_tera.render(template, &self.context);
268                    if let Err(ref e) = res {
269                        // Detailed log of the Tera error with all sources
270                        error!(
271                            template = template,
272                            error_kind = ?e.kind(),
273                            error_message = %e,
274                            "Tera rendering failed in debug mode"
275                        );
276
277                        // Log the full error chain (source)
278                        // Uses the source() method from the std::error::Error trait
279                        use std::error::Error as StdError;
280                        if let Some(source) = e.source() {
281                            error!(
282                                source_error = %source,
283                                "Tera error source"
284                            );
285                        }
286                    }
287                    res
288                }
289                Err(e) => {
290                    error!(
291                        template = template,
292                        error = %e,
293                        "Failed to initialize TemplateLoader in debug mode"
294                    );
295                    return Err(AppError::map_tera(
296                        tera::Error::message(e.to_string()),
297                        template,
298                        &self.engine.tera,
299                    ));
300                }
301            }
302        } else {
303            // Production mode: uses the already transformed instance
304            self.engine.tera.render(template, &self.context)
305        };
306
307        html_result
308            .map(|html| Html(html).into_response())
309            .map_err(|e| AppError::map_tera(e, template, &self.engine.tera))
310    }
311
312    /// Fluent insertion with builder pattern
313    pub fn insert(mut self, key: &str, value: impl serde::Serialize) -> Self {
314        // Tera 2 stores keys as `Cow<'static, str>`; the borrowed `&str` of this
315        // public signature is owned here so callers keep passing plain `&str`.
316        self.context.insert(key.to_string(), &value);
317        self
318    }
319
320    /// Loads the signed-in user's groups and rights from the database into
321    /// `self.user` and the template's `current_user`, for a view that checks a
322    /// right (`can_access_resource`, `permission_for`). Not loaded by default:
323    /// most pages never look at them.
324    pub async fn load_user_rights(&mut self) {
325        if let Some(user) = self.user.as_mut() {
326            user.groupes = crate::auth::pull_groupes_db(&self.engine.db, user.id).await;
327            self.context.insert("current_user", &*user);
328        }
329    }
330
331    /// Returns a reference to the database connection.
332    pub fn db(&self) -> &ADb {
333        &self.engine.db
334    }
335
336    /// Base of the absolute links an app sends out (activation, reset, emails):
337    /// the public URL set with `.with_public_url()`, else — in debug only —
338    /// `http://` + the request's `Host`. `None` in production without a public
339    /// URL: `Host` is the client's to choose, and a link built on it would carry
340    /// its token to someone else's site.
341    #[must_use]
342    pub fn public_url(&self) -> Option<String> {
343        crate::auth::password::public_link_base(
344            self.engine.config.server.public_url.as_deref(),
345            &self.headers,
346            self.engine.config.debug,
347        )
348    }
349
350    /// Returns a path segment as a string slice (`/users/{id}` → `get_path("id")`).
351    pub fn get_path(&self, key: &str) -> Option<&str> {
352        self.path_params.get(key).map(|s| s.as_str())
353    }
354
355    /// Parses a path segment into any type implementing `FromStr`.
356    pub fn get_path_as<T: FromStr>(&self, key: &str) -> Option<T> {
357        self.path_params.get(key)?.parse().ok()
358    }
359
360    /// Returns a query parameter as a string slice (`?page=2` → `get_query("page")`).
361    pub fn get_query(&self, key: &str) -> Option<&str> {
362        self.query_params.get(key).map(|s| s.as_str())
363    }
364
365    /// Deserializes the full query string into a typed struct deriving
366    /// `serde::Deserialize`. Unknown keys are ignored; empty values (`key=`)
367    /// are dropped, so an `Option` field gets `None` rather than failing.
368    ///
369    /// A query string that doesn't fit `T` (`?page=abc` for a `u32`) is a
370    /// `400 Bad Request`: `request.query::<Filters>()?` in a handler renders
371    /// `400.html` (overridable), with the reason on the debug page.
372    pub fn query<T: DeserializeOwned>(&self) -> AppResult<T> {
373        let cleaned = self
374            .raw_query
375            .split('&')
376            .filter(|pair| pair.split('=').nth(1).is_none_or(|v| !v.is_empty()))
377            .collect::<Vec<_>>()
378            .join("&");
379        serde_urlencoded::from_str(&cleaned).map_err(|e| {
380            Box::new(AppError::new(ErrorContext::bad_request(&tf(
381                "error.invalid_query",
382                &[e.to_string()],
383            ))))
384        })
385    }
386
387    /// Returns an `UrlParams` combining path and query — to be passed to `form.cleaned()`
388    pub fn url_params(&self) -> UrlParams<'_> {
389        UrlParams::new(&self.path_params, &self.query_params)
390    }
391
392    /// Constructs a form `T`, fills it with submitted data (POST body or query string on GET),
393    /// and injects the honeypot field if the anti-bot middleware is active.
394    /// Call `form.is_valid().await` before reading cleaned values or calling `save()`.
395    pub fn form<T: RuniqueForm>(&self) -> T {
396        let masked = self
397            .csrf_token
398            .masked()
399            .unwrap_or_else(|_| self.csrf_token.clone());
400        let mut form = T::build(self.engine.tera.clone(), masked.as_str());
401        form.get_form_mut()
402            .set_url_params(&self.path_params, &self.query_params);
403
404        if let Some(ref hp_name) = self.honeypot_field_name {
405            form.get_form_mut().set_honeypot(hp_name);
406            if self.method == Method::POST
407                && self.prisme.data.get(hp_name).is_some_and(|v| !v.is_empty())
408            {
409                form.get_form_mut().force_invalid = true;
410            }
411        }
412
413        // CSRF enforcement: Prisme computes `csrf_valid` but never rejects on its own.
414        // Without this, a mutating request with a missing/invalid token silently passes
415        // is_valid(). Fail closed on any non-safe method (same policy as the pipeline).
416        //
417        // `force_invalid` short-circuits `Forms::is_valid()` before field validation
418        // ever runs — `HiddenField::validate()`'s own CSRF branch (`csrf.missing`/
419        // `csrf.invalid`) never fires this way, and never did (its `expected_value`
420        // is set by nothing outside unit tests). Push a message here directly so a
421        // failed submission isn't silently indistinguishable from an untouched form —
422        // reuses the same key already shown by the admin CSRF check (`admin_router.rs`).
423        if csrf_required(&self.method) && !self.prisme.csrf_valid {
424            let csrf = form.get_form_mut();
425            csrf.force_invalid = true;
426            csrf.errors.push(t("csrf.invalid_or_missing").into_owned());
427        }
428
429        form.get_form_mut()
430            .fill(&self.prisme.data, self.method.clone());
431        form
432    }
433}