rustauth_core/api/
response_helpers.rs1use http::{header, HeaderValue, StatusCode};
2use serde::Serialize;
3use time::OffsetDateTime;
4
5use crate::context::request_state::{has_request_state, set_current_new_session};
6use crate::context::AuthContext;
7use crate::cookies::{
8 set_cookie_cache, set_session_cookie, Cookie, CookieCachePayload, CookieOptions,
9 SessionCookieOptions,
10};
11use crate::db::{Session, User};
12use crate::error::RustAuthError;
13
14use super::ApiResponse;
15
16pub fn json_response<T>(
17 status: StatusCode,
18 body: &T,
19 cookies: Vec<Cookie>,
20) -> Result<ApiResponse, RustAuthError>
21where
22 T: Serialize,
23{
24 let body = serde_json::to_vec(body).map_err(|error| RustAuthError::Serialization {
25 context: "serializing JSON response body",
26 message: error.to_string(),
27 })?;
28 let mut response = http::Response::builder()
29 .status(status)
30 .header(header::CONTENT_TYPE, "application/json")
31 .body(body)
32 .map_err(|error| RustAuthError::Serialization {
33 context: "building JSON response",
34 message: error.to_string(),
35 })?;
36 append_cookies(response.headers_mut(), cookies)?;
37 Ok(response)
38}
39
40pub fn redirect_response(
41 location: &str,
42 cookies: Vec<Cookie>,
43) -> Result<ApiResponse, RustAuthError> {
44 let mut response = http::Response::builder()
45 .status(StatusCode::FOUND)
46 .header(header::LOCATION, location)
47 .body(Vec::new())
48 .map_err(|error| RustAuthError::Serialization {
49 context: "building redirect response",
50 message: error.to_string(),
51 })?;
52 append_cookies(response.headers_mut(), cookies)?;
53 Ok(response)
54}
55
56pub fn redirect_with_error_response(
57 location: &str,
58 error: &str,
59) -> Result<ApiResponse, RustAuthError> {
60 let separator = if location.contains('?') { '&' } else { '?' };
61 redirect_response(
62 &format!(
63 "{location}{separator}error={}",
64 url::form_urlencoded::byte_serialize(error.as_bytes()).collect::<String>()
65 ),
66 Vec::new(),
67 )
68}
69
70pub fn session_cookies(
71 context: &AuthContext,
72 session: &Session,
73 user: &User,
74 dont_remember: bool,
75) -> Result<Vec<Cookie>, RustAuthError> {
76 if has_request_state() {
77 set_current_new_session(session.clone(), user.clone())?;
78 }
79 let mut cookies = set_session_cookie(
80 &context.auth_cookies,
81 &context.secret,
82 &session.token,
83 SessionCookieOptions {
84 dont_remember,
85 overrides: CookieOptions::default(),
86 },
87 )?;
88 if context.options.session.cookie_cache.enabled {
89 let max_age = context
90 .options
91 .session
92 .cookie_cache
93 .max_age
94 .unwrap_or(time::Duration::minutes(5));
95 cookies.extend(set_cookie_cache(
96 &context.auth_cookies,
97 &context.secret,
98 &CookieCachePayload {
99 session: session.clone(),
100 user: user.clone(),
101 updated_at: OffsetDateTime::now_utc().unix_timestamp(),
102 version: context
103 .options
104 .session
105 .cookie_cache
106 .version
107 .clone()
108 .unwrap_or_else(|| "1".to_owned()),
109 },
110 context.options.session.cookie_cache.strategy,
111 max_age.whole_seconds() as u64,
112 )?);
113 }
114 Ok(cookies)
115}
116
117pub fn append_cookies(
118 headers: &mut http::HeaderMap,
119 cookies: Vec<Cookie>,
120) -> Result<(), RustAuthError> {
121 for cookie in cookies {
122 headers.append(
123 header::SET_COOKIE,
124 HeaderValue::from_str(&serialize_cookie(&cookie))
125 .map_err(|error| RustAuthError::Cookie(error.to_string()))?,
126 );
127 }
128 Ok(())
129}
130
131pub fn serialize_cookie(cookie: &Cookie) -> String {
132 let mut parts = vec![format!("{}={}", cookie.name, cookie.value)];
133 if let Some(max_age) = cookie.attributes.max_age {
134 parts.push(format!("Max-Age={max_age}"));
135 }
136 if let Some(expires) = &cookie.attributes.expires {
137 parts.push(format!("Expires={expires}"));
138 }
139 if let Some(domain) = &cookie.attributes.domain {
140 parts.push(format!("Domain={domain}"));
141 }
142 if let Some(path) = &cookie.attributes.path {
143 parts.push(format!("Path={path}"));
144 }
145 if cookie.attributes.secure == Some(true) {
146 parts.push("Secure".to_owned());
147 }
148 if cookie.attributes.http_only == Some(true) {
149 parts.push("HttpOnly".to_owned());
150 }
151 if let Some(same_site) = &cookie.attributes.same_site {
152 parts.push(format!("SameSite={same_site}"));
153 }
154 if cookie.attributes.partitioned == Some(true) {
155 parts.push("Partitioned".to_owned());
156 }
157 parts.join("; ")
158}