use crate::error::{Error, Result};
#[cfg(any(feature = "api", feature = "inertia"))]
use axum::Json;
use axum::http::HeaderValue;
use axum::response::{IntoResponse, Redirect, Response};
#[derive(Debug, Clone)]
pub struct RedirectResponse {
target: RedirectTarget,
permanent: bool,
flash: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
enum RedirectTarget {
Path(String),
Route {
name: String,
params: Vec<RouteParam>,
},
Back,
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct RouteParam(pub(crate) String);
impl RedirectResponse {
#[must_use]
pub fn to(mut self, path: impl Into<String>) -> Self {
self.target = RedirectTarget::Path(path.into());
self
}
#[must_use]
pub fn route(mut self, name: impl Into<String>, params: impl IntoRouteParams) -> Self {
self.target = RedirectTarget::Route {
name: name.into(),
params: params.into_params(),
};
self
}
#[must_use]
pub fn back(mut self) -> Self {
self.target = RedirectTarget::Back;
self
}
#[must_use]
pub fn permanent(mut self) -> Self {
self.permanent = true;
self
}
#[must_use]
pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.flash.push((key.into(), value.into()));
self
}
pub(crate) fn resolve(
&self,
table: &crate::routing::RouteTable,
referer: Option<&str>,
) -> Result<Redirect> {
let path = match &self.target {
RedirectTarget::Path(p) => {
validate_redirect_target(p)?;
p.clone()
}
RedirectTarget::Route { name, params } => {
let params: Vec<&str> = params.iter().map(|RouteParam(s)| s.as_str()).collect();
table.url_for(name, ¶ms)?
}
RedirectTarget::Back => referer
.filter(|r| validate_redirect_target(r).is_ok())
.unwrap_or("/")
.to_string(),
};
Ok(if self.permanent {
Redirect::permanent(&path)
} else {
Redirect::temporary(&path)
})
}
#[cfg(feature = "auth")]
pub(crate) async fn persist_flash(&self, session: &tower_sessions::Session) {
if self.flash.is_empty() {
return;
}
let mut data: std::collections::BTreeMap<String, String> =
match session.get(crate::auth::FLASH_DATA_KEY).await {
Ok(existing) => existing.unwrap_or_default(),
Err(error) => return flash_write_failed(&error),
};
data.extend(self.flash.iter().cloned());
if let Err(error) = session.insert(crate::auth::FLASH_DATA_KEY, &data).await {
flash_write_failed(&error);
}
}
}
impl IntoResponse for RedirectResponse {
fn into_response(self) -> Response {
let mut response = match &self.target {
RedirectTarget::Path(p) => {
if validate_redirect_target(p).is_err() {
return Error::BadRequest("invalid redirect target".into()).into_response();
}
if self.permanent {
Redirect::permanent(p).into_response()
} else {
Redirect::temporary(p).into_response()
}
}
RedirectTarget::Route { .. } => {
Error::BadRequest("named-route redirect requires the route table".into())
.into_response()
}
RedirectTarget::Back => Redirect::to("/").into_response(),
};
response.extensions_mut().insert(self);
response
}
}
#[cfg(feature = "auth")]
fn flash_write_failed(error: &tower_sessions::session::Error) {
#[cfg(feature = "observe")]
tracing::warn!(%error, "flash data could not be stored; the redirect still happened");
#[cfg(not(feature = "observe"))]
let _ = error;
}
fn validate_redirect_target(path: &str) -> Result<()> {
if path.starts_with("//") || path.starts_with("http://") || path.starts_with("https://") {
return Err(Error::Redirect("external redirect not allowed".into()));
}
Ok(())
}
pub trait IntoRouteParams {
#[doc(hidden)]
fn into_params(self) -> Vec<RouteParam>;
}
impl IntoRouteParams for () {
fn into_params(self) -> Vec<RouteParam> {
Vec::new()
}
}
impl IntoRouteParams for &str {
fn into_params(self) -> Vec<RouteParam> {
vec![RouteParam(self.to_string())]
}
}
impl IntoRouteParams for String {
fn into_params(self) -> Vec<RouteParam> {
vec![RouteParam(self)]
}
}
impl IntoRouteParams for i64 {
fn into_params(self) -> Vec<RouteParam> {
vec![RouteParam(self.to_string())]
}
}
impl IntoRouteParams for u64 {
fn into_params(self) -> Vec<RouteParam> {
vec![RouteParam(self.to_string())]
}
}
#[cfg(feature = "database")]
impl IntoRouteParams for uuid::Uuid {
fn into_params(self) -> Vec<RouteParam> {
vec![RouteParam(self.to_string())]
}
}
impl<T: IntoRouteParams> IntoRouteParams for Vec<T> {
fn into_params(self) -> Vec<RouteParam> {
self.into_iter().flat_map(T::into_params).collect()
}
}
macro_rules! impl_into_route_params_tuple {
($($T:ident),* $(,)?) => {
impl<$($T: IntoRouteParams),*> IntoRouteParams for ($($T,)*) {
#[allow(non_snake_case, reason = "bindings are named after the type parameters")]
fn into_params(self) -> Vec<RouteParam> {
let ($($T,)*) = self;
let mut out = Vec::new();
$( out.extend($T.into_params()); )*
out
}
}
};
}
impl_into_route_params_tuple!(A);
impl_into_route_params_tuple!(A, B);
impl_into_route_params_tuple!(A, B, C);
impl_into_route_params_tuple!(A, B, C, D);
#[must_use]
pub fn redirect() -> RedirectResponse {
RedirectResponse {
target: RedirectTarget::Path("/".into()),
permanent: false,
flash: Vec::new(),
}
}
#[cfg(any(feature = "api", feature = "inertia"))]
pub fn json<T: serde::Serialize>(value: T) -> Response {
Json(value).into_response()
}
pub fn text<S: Into<String>>(status: axum::http::StatusCode, body: S) -> Response {
(
status,
[(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
)],
body.into(),
)
.into_response()
}
pub fn no_content() -> Response {
axum::http::StatusCode::NO_CONTENT.into_response()
}