1use 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
30pub struct AppError {
34 pub context: ErrorContext,
36}
37
38impl AppError {
39 pub fn new(context: ErrorContext) -> Self {
41 Self { context }
42 }
43
44 pub fn map_tera(e: tera::Error, route: &str, tera: &tera::Tera) -> Box<Self> {
46 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 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 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#[derive(Clone)]
93pub struct Request {
94 pub engine: AEngine,
96 pub session: Session,
98 pub notices: Message,
100 pub csrf_token: CsrfToken,
102 pub context: Context,
104 pub method: Method,
106 pub headers: axum::http::HeaderMap,
108 pub path_params: StrMap,
110 pub raw_query: String,
112 pub query_params: StrMap,
114 pub user: Option<CurrentUser>,
116 pub prisme: Prisme,
119 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 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 pub fn new(engine: AEngine, session: Session, csrf_token: CsrfToken, method: Method) -> Self {
225 let mut context = tera::Context::new();
226 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 csrf_valid: false,
255 },
256 honeypot_field_name: None,
257 }
258 }
259
260 pub fn render(&mut self, template: &str) -> AppResult<Response> {
262 let html_result = if self.engine.config.debug {
263 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 error!(
271 template = template,
272 error_kind = ?e.kind(),
273 error_message = %e,
274 "Tera rendering failed in debug mode"
275 );
276
277 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 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 pub fn insert(mut self, key: &str, value: impl serde::Serialize) -> Self {
314 self.context.insert(key.to_string(), &value);
317 self
318 }
319
320 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 pub fn db(&self) -> &ADb {
333 &self.engine.db
334 }
335
336 #[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 pub fn get_path(&self, key: &str) -> Option<&str> {
352 self.path_params.get(key).map(|s| s.as_str())
353 }
354
355 pub fn get_path_as<T: FromStr>(&self, key: &str) -> Option<T> {
357 self.path_params.get(key)?.parse().ok()
358 }
359
360 pub fn get_query(&self, key: &str) -> Option<&str> {
362 self.query_params.get(key).map(|s| s.as_str())
363 }
364
365 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 pub fn url_params(&self) -> UrlParams<'_> {
389 UrlParams::new(&self.path_params, &self.query_params)
390 }
391
392 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 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}