use crate::cookies::CookieJar;
use crate::flash::Flash;
use crate::session::{CookieSessionStore, EncryptedCookieSessionStore, Session};
use axum::{
body::Body,
extract::{FromRequestParts, RawPathParams, Request},
http::{header, HeaderValue, StatusCode},
response::Response,
};
use doido_model::sea_orm::DatabaseConnection;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::BTreeMap;
const MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
const SESSION_COOKIE: &str = "_doido_session";
const FLASH_COOKIE: &str = "_doido_flash";
pub struct Context {
pub(crate) parts: http::request::Parts,
pub(crate) body: Option<Body>,
pub(crate) path_params: Vec<(String, String)>,
pub(crate) session: Option<Session>,
pub(crate) flash: Option<Flash>,
pub(crate) flash_loaded: BTreeMap<String, String>,
pub(crate) cookies: Option<CookieJar>,
}
impl Context {
pub async fn build(req: Request) -> Self {
let (mut parts, body) = req.into_parts();
let path_params = Self::extract_path_params(&mut parts).await;
Self {
parts,
body: Some(body),
path_params,
session: None,
flash: None,
flash_loaded: BTreeMap::new(),
cookies: None,
}
}
async fn extract_path_params(parts: &mut http::request::Parts) -> Vec<(String, String)> {
match RawPathParams::from_request_parts(parts, &()).await {
Ok(params) => params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
Err(_) => Vec::new(),
}
}
pub fn from_request_parts(parts: http::request::Parts) -> Self {
Self {
parts,
body: None,
path_params: Vec::new(),
session: None,
flash: None,
flash_loaded: BTreeMap::new(),
cookies: None,
}
}
pub fn from_request(parts: http::request::Parts, body: Body) -> Self {
Self {
parts,
body: Some(body),
path_params: Vec::new(),
session: None,
flash: None,
flash_loaded: BTreeMap::new(),
cookies: None,
}
}
pub fn db(&self) -> &'static DatabaseConnection {
doido_model::pool::pool()
}
pub fn param(&self, name: &str) -> Option<&str> {
self.path_params
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
pub async fn form<T: DeserializeOwned>(&mut self) -> doido_core::Result<T> {
let bytes = self.read_body().await?;
serde_urlencoded::from_bytes(&bytes)
.map_err(|e| doido_core::anyhow::anyhow!("form deserialization failed: {e}"))
}
pub async fn body_json<T: DeserializeOwned>(&mut self) -> doido_core::Result<T> {
let bytes = self.read_body().await?;
serde_json::from_slice(&bytes)
.map_err(|e| doido_core::anyhow::anyhow!("JSON body deserialization failed: {e}"))
}
async fn read_body(&mut self) -> doido_core::Result<Vec<u8>> {
let body = self
.body
.take()
.ok_or_else(|| doido_core::anyhow::anyhow!("request body already consumed"))?;
let bytes = axum::body::to_bytes(body, MAX_BODY_BYTES)
.await
.map_err(|e| doido_core::anyhow::anyhow!("failed to read request body: {e}"))?;
Ok(bytes.to_vec())
}
pub fn params<T: serde::de::DeserializeOwned>(&self) -> doido_core::Result<T> {
let query = self.parts.uri.query().unwrap_or("");
serde_urlencoded::from_str(query)
.map_err(|e| doido_core::anyhow::anyhow!("params deserialization failed: {e}"))
}
pub fn query_params(&self) -> crate::params::Params {
let query = self.parts.uri.query().unwrap_or("");
let pairs: Vec<(String, String)> = serde_urlencoded::from_str(query).unwrap_or_default();
let mut map = serde_json::Map::new();
for (k, v) in pairs {
map.insert(k, serde_json::Value::String(v));
}
crate::params::Params::new(serde_json::Value::Object(map))
}
pub fn render(&self, template: &str, data: serde_json::Value) -> Response {
match doido_view::render(template, &data) {
Ok(html) => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(Body::from(html))
.expect("valid html response"),
Err(error) => {
tracing::error!(%error, template, "view render failed");
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Internal Server Error"))
.expect("valid 500 response")
}
}
}
pub fn json<T: Serialize>(&self, data: T) -> Response {
let body = serde_json::to_vec(&data).unwrap_or_default();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap()
}
pub fn redirect_to(&self, location: impl AsRef<str>) -> Response {
Response::builder()
.status(StatusCode::FOUND)
.header(
header::LOCATION,
HeaderValue::from_str(location.as_ref()).unwrap(),
)
.body(Body::empty())
.unwrap()
}
pub fn status(&self, code: u16) -> Response {
Response::builder()
.status(code)
.body(Body::empty())
.unwrap()
}
pub fn header(&self, name: &str) -> Option<&http::HeaderValue> {
self.parts.headers.get(name)
}
pub fn send_data(&self, data: Vec<u8>, content_type: &str, filename: Option<&str>) -> Response {
let mut builder = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type);
if let Some(name) = filename {
builder = builder.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{name}\""),
);
}
builder
.body(Body::from(data))
.expect("valid send_data response")
}
pub async fn send_file(
&self,
path: impl AsRef<std::path::Path>,
content_type: Option<&str>,
) -> doido_core::Result<Response> {
let path = path.as_ref();
let data = tokio::fs::read(path)
.await
.map_err(|e| doido_core::anyhow::anyhow!("send_file failed to read {path:?}: {e}"))?;
let content_type = content_type.unwrap_or("application/octet-stream");
let filename = path.file_name().and_then(|n| n.to_str());
Ok(self.send_data(data, content_type, filename))
}
pub fn negotiated_format(&self) -> crate::respond::Format {
use crate::respond::Format;
let path = self.parts.uri.path();
if path.ends_with(".json") {
return Format::Json;
}
if path.ends_with(".html") {
return Format::Html;
}
match self
.parts
.headers
.get(header::ACCEPT)
.and_then(|a| a.to_str().ok())
{
Some(accept) if accept.contains("application/json") => Format::Json,
Some(accept) if accept.contains("text/html") => Format::Html,
_ => Format::Any,
}
}
pub fn respond_to(&self) -> crate::respond::RespondTo {
crate::respond::RespondTo::new(self.negotiated_format())
}
pub fn etag_matches(&self, etag: &str) -> bool {
match self
.parts
.headers
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok())
{
Some("*") => true,
Some(inm) => inm.split(',').map(str::trim).any(|t| t == etag),
None => false,
}
}
fn if_modified_since_matches(&self, last_modified: &str) -> bool {
self.parts
.headers
.get(header::IF_MODIFIED_SINCE)
.and_then(|v| v.to_str().ok())
.map(|v| v == last_modified)
.unwrap_or(false)
}
pub fn fresh_when(&self, etag: Option<&str>, last_modified: Option<&str>) -> Option<Response> {
let fresh = etag.map(|e| self.etag_matches(e)).unwrap_or(false)
|| last_modified
.map(|lm| self.if_modified_since_matches(lm))
.unwrap_or(false);
if !fresh {
return None;
}
let mut builder = Response::builder().status(StatusCode::NOT_MODIFIED);
if let Some(e) = etag {
builder = builder.header(header::ETAG, e);
}
if let Some(lm) = last_modified {
builder = builder.header(header::LAST_MODIFIED, lm);
}
Some(builder.body(Body::empty()).expect("valid 304 response"))
}
pub fn cookies(&mut self) -> &mut CookieJar {
if self.cookies.is_none() {
let header = self
.parts
.headers
.get(header::COOKIE)
.and_then(|v| v.to_str().ok());
self.cookies = Some(CookieJar::from_header(header, crate::secret::key_base()));
}
self.cookies.as_mut().expect("cookie jar just set")
}
pub fn session(&mut self) -> &mut Session {
if self.session.is_none() {
let store = EncryptedCookieSessionStore::default();
let session = self
.raw_cookie(SESSION_COOKIE)
.and_then(|raw| store.decode(&raw))
.unwrap_or_default();
self.session = Some(session);
}
self.session.as_mut().expect("session just set")
}
pub fn flash(&mut self) -> &mut Flash {
if self.flash.is_none() {
let store = CookieSessionStore::new(crate::secret::key_base());
let flash = self
.raw_cookie(FLASH_COOKIE)
.map(|raw| Flash::from_cookie(&store, &raw))
.unwrap_or_default();
self.flash_loaded = flash.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
self.flash = Some(flash);
}
self.flash.as_mut().expect("flash just set")
}
fn raw_cookie(&self, name: &str) -> Option<String> {
let header = self.parts.headers.get(header::COOKIE)?.to_str().ok()?;
header
.split(';')
.filter_map(|pair| pair.trim().split_once('='))
.find(|(k, _)| *k == name)
.map(|(_, v)| v.to_string())
}
pub fn commit_to_response(&self, response: &mut Response) {
let secret = crate::secret::key_base();
if let Some(session) = &self.session {
let value = EncryptedCookieSessionStore::new(secret.clone()).encode(session);
append_cookie(
response,
&format!("{SESSION_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax"),
);
}
if let Some(flash) = &self.flash {
let current: BTreeMap<String, String> =
flash.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
if current != self.flash_loaded {
if current.is_empty() {
append_cookie(response, &format!("{FLASH_COOKIE}=; Path=/; Max-Age=0"));
} else {
let value = flash.to_cookie(&CookieSessionStore::new(secret.clone()));
append_cookie(
response,
&format!("{FLASH_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax"),
);
}
} else if !self.flash_loaded.is_empty() {
append_cookie(response, &format!("{FLASH_COOKIE}=; Path=/; Max-Age=0"));
}
}
if let Some(jar) = &self.cookies {
for header_value in jar.to_set_cookie_headers() {
append_cookie(response, &header_value);
}
}
}
}
fn append_cookie(response: &mut Response, value: &str) {
if let Ok(header_value) = HeaderValue::from_str(value) {
response
.headers_mut()
.append(header::SET_COOKIE, header_value);
}
}
pub trait IntoActionResponse {
fn into_action_response(self) -> Response;
}
impl IntoActionResponse for Response {
fn into_action_response(self) -> Response {
self
}
}
impl<E: std::fmt::Display> IntoActionResponse for Result<Response, E> {
fn into_action_response(self) -> Response {
match self {
Ok(response) => response,
Err(error) => {
tracing::error!(%error, "action returned an error");
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Internal Server Error"))
.expect("static 500 response is valid")
}
}
}
}
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<Context>();
};