1use crate::cookies::CookieJar;
2use crate::flash::Flash;
3use crate::session::{CookieSessionStore, EncryptedCookieSessionStore, Session};
4use axum::{
5 body::Body,
6 extract::{FromRequestParts, RawPathParams, Request},
7 http::{header, HeaderValue, StatusCode},
8 response::Response,
9};
10use doido_model::sea_orm::DatabaseConnection;
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13use std::collections::BTreeMap;
14
15const MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
17
18const SESSION_COOKIE: &str = "_doido_session";
20const FLASH_COOKIE: &str = "_doido_flash";
22
23pub struct Context {
25 pub(crate) parts: http::request::Parts,
26 pub(crate) body: Option<Body>,
28 pub(crate) path_params: Vec<(String, String)>,
30 pub(crate) session: Option<Session>,
32 pub(crate) flash: Option<Flash>,
34 pub(crate) flash_loaded: BTreeMap<String, String>,
36 pub(crate) cookies: Option<CookieJar>,
38}
39
40impl Context {
41 pub async fn build(req: Request) -> Self {
44 let (mut parts, body) = req.into_parts();
45 let path_params = Self::extract_path_params(&mut parts).await;
46 Self {
47 parts,
48 body: Some(body),
49 path_params,
50 session: None,
51 flash: None,
52 flash_loaded: BTreeMap::new(),
53 cookies: None,
54 }
55 }
56
57 async fn extract_path_params(parts: &mut http::request::Parts) -> Vec<(String, String)> {
58 match RawPathParams::from_request_parts(parts, &()).await {
59 Ok(params) => params
60 .iter()
61 .map(|(k, v)| (k.to_string(), v.to_string()))
62 .collect(),
63 Err(_) => Vec::new(),
64 }
65 }
66
67 pub fn from_request_parts(parts: http::request::Parts) -> Self {
68 Self {
69 parts,
70 body: None,
71 path_params: Vec::new(),
72 session: None,
73 flash: None,
74 flash_loaded: BTreeMap::new(),
75 cookies: None,
76 }
77 }
78
79 pub fn from_request(parts: http::request::Parts, body: Body) -> Self {
80 Self {
81 parts,
82 body: Some(body),
83 path_params: Vec::new(),
84 session: None,
85 flash: None,
86 flash_loaded: BTreeMap::new(),
87 cookies: None,
88 }
89 }
90
91 pub fn db(&self) -> &'static DatabaseConnection {
93 doido_model::pool::pool()
94 }
95
96 pub fn param(&self, name: &str) -> Option<&str> {
98 self.path_params
99 .iter()
100 .find(|(k, _)| k == name)
101 .map(|(_, v)| v.as_str())
102 }
103
104 pub async fn form<T: DeserializeOwned>(&mut self) -> doido_core::Result<T> {
106 let bytes = self.read_body().await?;
107 serde_urlencoded::from_bytes(&bytes)
108 .map_err(|e| doido_core::anyhow::anyhow!("form deserialization failed: {e}"))
109 }
110
111 pub async fn body_json<T: DeserializeOwned>(&mut self) -> doido_core::Result<T> {
113 let bytes = self.read_body().await?;
114 serde_json::from_slice(&bytes)
115 .map_err(|e| doido_core::anyhow::anyhow!("JSON body deserialization failed: {e}"))
116 }
117
118 async fn read_body(&mut self) -> doido_core::Result<Vec<u8>> {
119 let body = self
120 .body
121 .take()
122 .ok_or_else(|| doido_core::anyhow::anyhow!("request body already consumed"))?;
123 let bytes = axum::body::to_bytes(body, MAX_BODY_BYTES)
124 .await
125 .map_err(|e| doido_core::anyhow::anyhow!("failed to read request body: {e}"))?;
126 Ok(bytes.to_vec())
127 }
128
129 pub fn params<T: serde::de::DeserializeOwned>(&self) -> doido_core::Result<T> {
131 let query = self.parts.uri.query().unwrap_or("");
132 serde_urlencoded::from_str(query)
133 .map_err(|e| doido_core::anyhow::anyhow!("params deserialization failed: {e}"))
134 }
135
136 pub fn query_params(&self) -> crate::params::Params {
139 let query = self.parts.uri.query().unwrap_or("");
140 let pairs: Vec<(String, String)> = serde_urlencoded::from_str(query).unwrap_or_default();
141 let mut map = serde_json::Map::new();
142 for (k, v) in pairs {
143 map.insert(k, serde_json::Value::String(v));
144 }
145 crate::params::Params::new(serde_json::Value::Object(map))
146 }
147
148 pub fn render(&self, template: &str, data: serde_json::Value) -> Response {
155 match doido_view::render(template, &data) {
156 Ok(html) => Response::builder()
157 .status(StatusCode::OK)
158 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
159 .body(Body::from(html))
160 .expect("valid html response"),
161 Err(error) => {
162 tracing::error!(%error, template, "view render failed");
163 Response::builder()
164 .status(StatusCode::INTERNAL_SERVER_ERROR)
165 .body(Body::from("Internal Server Error"))
166 .expect("valid 500 response")
167 }
168 }
169 }
170
171 pub fn json<T: Serialize>(&self, data: T) -> Response {
173 let body = serde_json::to_vec(&data).unwrap_or_default();
174 Response::builder()
175 .status(StatusCode::OK)
176 .header(header::CONTENT_TYPE, "application/json")
177 .body(Body::from(body))
178 .unwrap()
179 }
180
181 pub fn redirect_to(&self, location: impl AsRef<str>) -> Response {
183 Response::builder()
184 .status(StatusCode::FOUND)
185 .header(
186 header::LOCATION,
187 HeaderValue::from_str(location.as_ref()).unwrap(),
188 )
189 .body(Body::empty())
190 .unwrap()
191 }
192
193 pub fn status(&self, code: u16) -> Response {
196 Response::builder()
197 .status(code)
198 .body(Body::empty())
199 .unwrap()
200 }
201
202 pub fn header(&self, name: &str) -> Option<&http::HeaderValue> {
204 self.parts.headers.get(name)
205 }
206
207 pub fn send_data(&self, data: Vec<u8>, content_type: &str, filename: Option<&str>) -> Response {
210 let mut builder = Response::builder()
211 .status(StatusCode::OK)
212 .header(header::CONTENT_TYPE, content_type);
213 if let Some(name) = filename {
214 builder = builder.header(
215 header::CONTENT_DISPOSITION,
216 format!("attachment; filename=\"{name}\""),
217 );
218 }
219 builder
220 .body(Body::from(data))
221 .expect("valid send_data response")
222 }
223
224 pub async fn send_file(
228 &self,
229 path: impl AsRef<std::path::Path>,
230 content_type: Option<&str>,
231 ) -> doido_core::Result<Response> {
232 let path = path.as_ref();
233 let data = tokio::fs::read(path)
234 .await
235 .map_err(|e| doido_core::anyhow::anyhow!("send_file failed to read {path:?}: {e}"))?;
236 let content_type = content_type.unwrap_or("application/octet-stream");
237 let filename = path.file_name().and_then(|n| n.to_str());
238 Ok(self.send_data(data, content_type, filename))
239 }
240
241 pub fn negotiated_format(&self) -> crate::respond::Format {
245 use crate::respond::Format;
246 let path = self.parts.uri.path();
247 if path.ends_with(".json") {
248 return Format::Json;
249 }
250 if path.ends_with(".html") {
251 return Format::Html;
252 }
253 match self
254 .parts
255 .headers
256 .get(header::ACCEPT)
257 .and_then(|a| a.to_str().ok())
258 {
259 Some(accept) if accept.contains("application/json") => Format::Json,
260 Some(accept) if accept.contains("text/html") => Format::Html,
261 _ => Format::Any,
262 }
263 }
264
265 pub fn respond_to(&self) -> crate::respond::RespondTo {
267 crate::respond::RespondTo::new(self.negotiated_format())
268 }
269
270 pub fn etag_matches(&self, etag: &str) -> bool {
272 match self
273 .parts
274 .headers
275 .get(header::IF_NONE_MATCH)
276 .and_then(|v| v.to_str().ok())
277 {
278 Some("*") => true,
279 Some(inm) => inm.split(',').map(str::trim).any(|t| t == etag),
280 None => false,
281 }
282 }
283
284 fn if_modified_since_matches(&self, last_modified: &str) -> bool {
285 self.parts
286 .headers
287 .get(header::IF_MODIFIED_SINCE)
288 .and_then(|v| v.to_str().ok())
289 .map(|v| v == last_modified)
290 .unwrap_or(false)
291 }
292
293 pub fn fresh_when(&self, etag: Option<&str>, last_modified: Option<&str>) -> Option<Response> {
298 let fresh = etag.map(|e| self.etag_matches(e)).unwrap_or(false)
299 || last_modified
300 .map(|lm| self.if_modified_since_matches(lm))
301 .unwrap_or(false);
302 if !fresh {
303 return None;
304 }
305 let mut builder = Response::builder().status(StatusCode::NOT_MODIFIED);
306 if let Some(e) = etag {
307 builder = builder.header(header::ETAG, e);
308 }
309 if let Some(lm) = last_modified {
310 builder = builder.header(header::LAST_MODIFIED, lm);
311 }
312 Some(builder.body(Body::empty()).expect("valid 304 response"))
313 }
314
315 pub fn cookies(&mut self) -> &mut CookieJar {
321 if self.cookies.is_none() {
322 let header = self
323 .parts
324 .headers
325 .get(header::COOKIE)
326 .and_then(|v| v.to_str().ok());
327 self.cookies = Some(CookieJar::from_header(header, crate::secret::key_base()));
328 }
329 self.cookies.as_mut().expect("cookie jar just set")
330 }
331
332 pub fn session(&mut self) -> &mut Session {
335 if self.session.is_none() {
336 let store = EncryptedCookieSessionStore::default();
337 let session = self
338 .raw_cookie(SESSION_COOKIE)
339 .and_then(|raw| store.decode(&raw))
340 .unwrap_or_default();
341 self.session = Some(session);
342 }
343 self.session.as_mut().expect("session just set")
344 }
345
346 pub fn flash(&mut self) -> &mut Flash {
350 if self.flash.is_none() {
351 let store = CookieSessionStore::new(crate::secret::key_base());
352 let flash = self
353 .raw_cookie(FLASH_COOKIE)
354 .map(|raw| Flash::from_cookie(&store, &raw))
355 .unwrap_or_default();
356 self.flash_loaded = flash.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
357 self.flash = Some(flash);
358 }
359 self.flash.as_mut().expect("flash just set")
360 }
361
362 fn raw_cookie(&self, name: &str) -> Option<String> {
364 let header = self.parts.headers.get(header::COOKIE)?.to_str().ok()?;
365 header
366 .split(';')
367 .filter_map(|pair| pair.trim().split_once('='))
368 .find(|(k, _)| *k == name)
369 .map(|(_, v)| v.to_string())
370 }
371
372 pub fn commit_to_response(&self, response: &mut Response) {
376 let secret = crate::secret::key_base();
377
378 if let Some(session) = &self.session {
379 let value = EncryptedCookieSessionStore::new(secret.clone()).encode(session);
380 append_cookie(
381 response,
382 &format!("{SESSION_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax"),
383 );
384 }
385
386 if let Some(flash) = &self.flash {
387 let current: BTreeMap<String, String> =
388 flash.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
389 if current != self.flash_loaded {
390 if current.is_empty() {
392 append_cookie(response, &format!("{FLASH_COOKIE}=; Path=/; Max-Age=0"));
393 } else {
394 let value = flash.to_cookie(&CookieSessionStore::new(secret.clone()));
395 append_cookie(
396 response,
397 &format!("{FLASH_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax"),
398 );
399 }
400 } else if !self.flash_loaded.is_empty() {
401 append_cookie(response, &format!("{FLASH_COOKIE}=; Path=/; Max-Age=0"));
403 }
404 }
405
406 if let Some(jar) = &self.cookies {
407 for header_value in jar.to_set_cookie_headers() {
408 append_cookie(response, &header_value);
409 }
410 }
411 }
412}
413
414fn append_cookie(response: &mut Response, value: &str) {
416 if let Ok(header_value) = HeaderValue::from_str(value) {
417 response
418 .headers_mut()
419 .append(header::SET_COOKIE, header_value);
420 }
421}
422
423pub trait IntoActionResponse {
428 fn into_action_response(self) -> Response;
429}
430
431impl IntoActionResponse for Response {
432 fn into_action_response(self) -> Response {
433 self
434 }
435}
436
437impl<E: std::fmt::Display> IntoActionResponse for Result<Response, E> {
438 fn into_action_response(self) -> Response {
439 match self {
440 Ok(response) => response,
441 Err(error) => {
442 tracing::error!(%error, "action returned an error");
443 Response::builder()
444 .status(StatusCode::INTERNAL_SERVER_ERROR)
445 .body(Body::from("Internal Server Error"))
446 .expect("static 500 response is valid")
447 }
448 }
449 }
450}
451
452const _: fn() = || {
455 fn assert_send<T: Send>() {}
456 assert_send::<Context>();
457};