use crate::app::templates::TemplateLoader;
use crate::auth::session::CurrentUser;
use crate::errors::error::ErrorContext;
use crate::flash::Message;
use crate::forms::{
extractor::{Prisme, csrf_required, prisme_pipeline},
field::RuniqueForm,
};
use crate::impl_from_error;
use crate::middleware::security::anti_bot::HoneypotFieldName;
use crate::utils::aliases::{AEngine, AppResult};
use crate::utils::url_params::UrlParams;
use crate::utils::{csp_nonce::CspNonce, csrf::CsrfToken};
use axum::{
body::Body,
extract::{FromRequest, FromRequestParts, Path},
http::{Request as HttpRequest, StatusCode, method::Method},
response::{Html, IntoResponse, Response},
};
use sea_orm::DbErr;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tera::Context;
use tower_sessions::Session;
use tracing::error;
pub struct AppError {
pub context: ErrorContext,
}
impl AppError {
pub fn new(context: ErrorContext) -> Self {
Self { context }
}
pub fn map_tera(e: tera::Error, route: &str, tera: &tera::Tera) -> Box<Self> {
error!(
template = route,
error = ?e,
"Template rendering error"
);
Box::new(Self {
context: ErrorContext::from_tera_error(&e, route, tera),
})
}
}
impl_from_error!(anyhow::Error => from_anyhow, DbErr => database);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.context.status_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
error!(
status = status.as_u16(),
error_type = ?self.context.error_type,
message = %self.context.message,
"AppError occurred"
);
let mut res = status.into_response();
res.extensions_mut().insert(Arc::new(self.context));
res
}
}
impl IntoResponse for Box<AppError> {
fn into_response(self) -> Response {
(*self).into_response()
}
}
#[derive(Clone)]
pub struct Request {
pub engine: AEngine,
pub session: Session,
pub notices: Message,
pub csrf_token: CsrfToken,
pub context: Context,
pub method: Method,
pub headers: axum::http::HeaderMap,
pub path_params: HashMap<String, String>,
pub raw_query: String,
pub query_params: HashMap<String, String>,
pub user: Option<CurrentUser>,
pub prisme: Prisme,
pub honeypot_field_name: Option<String>,
}
impl<S> FromRequest<S> for Request
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: HttpRequest<Body>, state: &S) -> Result<Self, Self::Rejection> {
let err = |msg: &str| (StatusCode::INTERNAL_SERVER_ERROR, msg.to_string()).into_response();
let (mut parts, body) = req.into_parts();
let ex = &parts.extensions;
let engine = ex
.get::<AEngine>()
.cloned()
.ok_or_else(|| err("engine missing"))?;
let csrf_token = ex
.get::<CsrfToken>()
.cloned()
.ok_or_else(|| err("csrf missing"))?;
let session = ex
.get::<Session>()
.cloned()
.ok_or_else(|| err("session missing"))?;
let nonce = ex.get::<CspNonce>().map(|n| n.as_str()).unwrap_or_default();
let user = ex.get::<CurrentUser>().cloned();
let honeypot_field_name = ex.get::<HoneypotFieldName>().map(|h| h.0.clone());
let notices = Message {
session: session.clone(),
};
let messages = notices.get_all().await;
let mut context = Context::new();
context.insert("debug", &engine.config.debug);
context.insert(
"csrf_token",
&csrf_token
.masked()
.unwrap_or_else(|_| csrf_token.clone())
.as_str(),
);
context.insert("csp_nonce", nonce);
context.insert("static_runique", &engine.config.static_files);
context.insert("messages", &messages);
if let Some(ref u) = user {
context.insert("current_user", u);
}
let path_params = Path::<HashMap<String, String>>::from_request_parts(&mut parts, state)
.await
.map(|Path(p)| p)
.unwrap_or_default();
let ico_image = std::env::var("ICON_IMAGE")
.unwrap_or("/runique/static/favicon_runique.ico".to_string());
let ico_image =
crate::utils::resolve_og_image(&engine.security_hosts, engine.config.debug, &ico_image);
context.insert("icon_image", &ico_image);
let og_image =
std::env::var("OG_IMAGE").unwrap_or("/runique/static/runique_320.avif".to_string());
let og_image =
crate::utils::resolve_og_image(&engine.security_hosts, engine.config.debug, &og_image);
context.insert("og_image", &og_image);
context.insert("current_path", parts.uri.path());
let raw_query = parts.uri.query().unwrap_or_default().to_string();
let query_params =
serde_urlencoded::from_str::<HashMap<String, String>>(&raw_query).unwrap_or_default();
let method = parts.method.clone();
let headers = parts.headers.clone();
let req = HttpRequest::from_parts(parts, body);
let prisme = prisme_pipeline(req, state).await?;
Ok(Self {
engine,
session,
notices,
csrf_token,
context,
method,
headers,
path_params,
raw_query,
query_params,
user,
prisme,
honeypot_field_name,
})
}
}
impl Request {
pub fn new(engine: AEngine, session: Session, csrf_token: CsrfToken, method: Method) -> Self {
let mut context = tera::Context::new();
context.insert("debug", &engine.config.debug);
context.insert("static_runique", &engine.config.static_files);
context.insert(
"csrf_token",
&csrf_token
.masked()
.unwrap_or_else(|_| csrf_token.clone())
.as_str(),
);
Self {
engine,
session: session.clone(),
notices: Message { session },
csrf_token,
context,
method,
headers: axum::http::HeaderMap::new(),
path_params: HashMap::new(),
raw_query: String::new(),
query_params: HashMap::new(),
user: None,
prisme: Prisme {
data: Default::default(),
csrf_valid: true,
},
honeypot_field_name: None,
}
}
pub fn is_get(&self) -> bool {
self.method == Method::GET
}
pub fn is_post(&self) -> bool {
self.method == Method::POST
}
pub fn is_put(&self) -> bool {
self.method == Method::PUT
}
pub fn is_delete(&self) -> bool {
self.method == Method::DELETE
}
pub fn render(&mut self, template: &str) -> AppResult<Response> {
let html_result = if self.engine.config.debug {
match TemplateLoader::init(&self.engine.config, self.engine.url_registry.clone()) {
Ok(dev_tera) => {
let res = dev_tera.render(template, &self.context);
if let Err(ref e) = res {
error!(
template = template,
error_kind = ?e.kind(),
error_message = %e,
"Tera rendering failed in debug mode"
);
use std::error::Error as StdError;
if let Some(source) = e.source() {
error!(
source_error = %source,
"Tera error source"
);
}
}
res
}
Err(e) => {
error!(
template = template,
error = %e,
"Failed to initialize TemplateLoader in debug mode"
);
return Err(AppError::map_tera(
tera::Error::message(e.to_string()),
template,
&self.engine.tera,
));
}
}
} else {
self.engine.tera.render(template, &self.context)
};
html_result
.map(|html| Html(html).into_response())
.map_err(|e| AppError::map_tera(e, template, &self.engine.tera))
}
pub fn insert(mut self, key: &str, value: impl serde::Serialize) -> Self {
self.context.insert(key.to_string(), &value);
self
}
pub fn render_with(
mut self,
template: &str,
data: Vec<(&str, serde_json::Value)>,
) -> AppResult<Response> {
for (k, v) in data {
self.context.insert(k.to_string(), &v);
}
self.render(template)
}
pub fn db(&self) -> &sea_orm::DatabaseConnection {
&self.engine.db
}
pub fn get_path(&self, key: &str) -> Option<&str> {
self.path_params.get(key).map(|s| s.as_str())
}
pub fn get_path_as<T: FromStr>(&self, key: &str) -> Option<T> {
self.path_params.get(key)?.parse().ok()
}
pub fn get_query(&self, key: &str) -> Option<&str> {
self.query_params.get(key).map(|s| s.as_str())
}
pub fn query<T: DeserializeOwned + Default>(&self) -> T {
let cleaned = self
.raw_query
.split('&')
.filter(|pair| pair.split('=').nth(1).is_none_or(|v| !v.is_empty()))
.collect::<Vec<_>>()
.join("&");
serde_urlencoded::from_str(&cleaned).unwrap_or_default()
}
pub fn url_params(&self) -> UrlParams<'_> {
UrlParams::new(&self.path_params, &self.query_params)
}
pub fn form<T: RuniqueForm>(&self) -> T {
let masked = self
.csrf_token
.masked()
.unwrap_or_else(|_| self.csrf_token.clone());
let mut form = T::build(self.engine.tera.clone(), masked.as_str());
form.get_form_mut()
.set_url_params(&self.path_params, &self.query_params);
if let Some(ref hp_name) = self.honeypot_field_name {
form.get_form_mut().set_honeypot(hp_name);
if self.is_post() && self.prisme.data.get(hp_name).is_some_and(|v| !v.is_empty()) {
form.get_form_mut().force_invalid = true;
}
}
if csrf_required(&self.method) && !self.prisme.csrf_valid {
form.get_form_mut().force_invalid = true;
}
form.get_form_mut()
.fill(&self.prisme.data, self.method.clone());
form
}
}