1use crate::AuthContext;
4use crate::session::SessionRequest;
5use crate::types::RequestExtensions;
6use crate::utils::LockUnpoisoned;
7use crate::utils::json::JsValue;
8use crate::wire::{SessionView, UserView};
9use crate::{AuthError, AuthRequest, AuthResult, AuthSchema, Headers, HttpMethod};
10use serde::{Serialize, de::DeserializeOwned};
11use std::collections::HashMap;
12use std::marker::PhantomData;
13use std::sync::{Arc, Mutex};
14
15#[derive(Clone, Debug)]
17pub struct EndpointDefinition {
18 pub name: &'static str,
19 pub operation_id: &'static str,
20 pub path: Option<String>,
21 pub method: HttpMethod,
22}
23
24#[derive(Clone, Debug)]
26pub struct ServerEndpoint<T> {
27 plugin: &'static str,
28 name: &'static str,
29 body: Option<JsValue>,
30 query: Option<JsValue>,
31 params: Option<JsValue>,
32 output: PhantomData<fn() -> T>,
33}
34
35impl<T> ServerEndpoint<T> {
36 #[must_use]
37 pub const fn new(plugin: &'static str, name: &'static str) -> Self {
38 Self {
39 plugin,
40 name,
41 body: None,
42 query: None,
43 params: None,
44 output: PhantomData,
45 }
46 }
47
48 pub fn with_body(mut self, body: &impl Serialize) -> AuthResult<Self> {
51 self.body = Some(crate::utils::json::parse_value(
52 &crate::utils::json::to_string(body)?,
53 )?);
54 Ok(self)
55 }
56
57 pub fn with_query(mut self, query: &impl Serialize) -> AuthResult<Self> {
60 self.query = Some(crate::utils::json::parse_value(
61 &crate::utils::json::to_string(query)?,
62 )?);
63 Ok(self)
64 }
65
66 #[must_use]
67 pub fn with_body_value(mut self, body: JsValue) -> Self {
68 self.body = Some(body);
69 self
70 }
71
72 #[must_use]
73 pub fn with_query_value(mut self, query: JsValue) -> Self {
74 self.query = Some(query);
75 self
76 }
77
78 pub fn with_params(mut self, params: &impl Serialize) -> AuthResult<Self> {
83 self.params = Some(crate::utils::json::parse_value(
84 &crate::utils::json::to_string(params)?,
85 )?);
86 Ok(self)
87 }
88
89 #[must_use]
90 pub fn with_params_value(mut self, params: JsValue) -> Self {
91 self.params = Some(params);
92 self
93 }
94
95 #[must_use]
96 pub const fn params(&self) -> Option<&JsValue> {
97 self.params.as_ref()
98 }
99
100 #[must_use]
101 pub const fn plugin(&self) -> &'static str {
102 self.plugin
103 }
104
105 #[must_use]
106 pub const fn name(&self) -> &'static str {
107 self.name
108 }
109
110 #[must_use]
111 pub fn input(self) -> (Option<JsValue>, Option<JsValue>) {
112 (self.body, self.query)
113 }
114}
115
116#[derive(Clone, Debug, Default)]
118pub struct EndpointOptions {
119 pub headers: Option<HashMap<String, String>>,
120 pub request: Option<AuthRequest>,
121 pub method: Option<HttpMethod>,
122}
123
124#[derive(Clone, Debug, Default)]
125struct EndpointState {
126 session: Option<SessionView>,
127 principal: Option<(UserView, SessionView)>,
128 response_headers: Headers,
129 session_observation: Option<(UserView, SessionView)>,
130}
131
132struct VerifiedEndpointUser<S: AuthSchema> {
133 user: S::User,
134 config: Arc<crate::AuthConfig>,
135 database: Arc<dyn crate::AuthStore<S>>,
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum EndpointPhase {
141 Raw,
142 Middleware,
143 Handler,
144}
145
146#[derive(Clone, Debug)]
152pub struct EndpointCall {
153 operation_id: String,
154 path: Option<String>,
155 method: Option<HttpMethod>,
156 body: Option<JsValue>,
157 query: Option<JsValue>,
158 params: Option<JsValue>,
159 headers: Option<HashMap<String, String>>,
160 request: Option<AuthRequest>,
161 default_method: HttpMethod,
162 phase: EndpointPhase,
163 extensions: RequestExtensions,
164 state: Arc<Mutex<EndpointState>>,
165}
166
167impl EndpointCall {
168 #[must_use]
169 pub fn new(
170 definition: &EndpointDefinition,
171 body: Option<JsValue>,
172 query: Option<JsValue>,
173 options: EndpointOptions,
174 ) -> Self {
175 Self {
176 operation_id: definition.operation_id.into(),
177 path: definition.path.clone(),
178 method: options.method,
179 body,
180 query,
181 params: None,
182 headers: options.headers.map(|headers| {
183 headers
184 .into_iter()
185 .map(|(name, value)| (name.to_ascii_lowercase(), value))
186 .collect()
187 }),
188 request: options.request.map(fresh_request),
189 default_method: definition.method.clone(),
190 phase: EndpointPhase::Raw,
191 extensions: RequestExtensions::default(),
192 state: Arc::new(Mutex::new(EndpointState::default())),
193 }
194 }
195
196 #[must_use]
199 pub fn from_http_request(request: &AuthRequest, route: &crate::AuthRoute) -> Self {
200 let body = request
201 .extensions()
202 .get::<crate::types::ParsedRequestBody>()
203 .and_then(|body| match body.as_ref() {
204 crate::types::ParsedRequestBody::Value(value) => Some(value.clone()),
205 crate::types::ParsedRequestBody::Opaque(_) => None,
206 });
207 let query = request.query_as_js_value();
208 let definition = EndpointDefinition {
209 name: "http",
210 operation_id: "http",
211 path: Some(route.context_path.as_ref().unwrap_or(&route.path).clone()),
212 method: route.method.clone(),
213 };
214 let mut call = Self::new(
215 &definition,
216 body,
217 Some(query),
218 EndpointOptions {
219 headers: Some(request.headers.clone()),
220 request: Some(
221 crate::hooks::current_request_hook_context()
222 .map_or_else(|| request.clone(), |context| context.request),
223 ),
224 method: Some(request.method.clone()),
225 },
226 );
227 call.operation_id.clone_from(&route.operation_id);
228 call
229 }
230
231 #[must_use]
232 pub fn operation_id(&self) -> &str {
233 &self.operation_id
234 }
235
236 #[must_use]
237 pub fn path(&self) -> Option<&str> {
238 self.path.as_deref()
239 }
240
241 #[must_use]
242 pub const fn method(&self) -> Option<&HttpMethod> {
243 self.method.as_ref()
244 }
245
246 #[must_use]
247 pub const fn body(&self) -> Option<&JsValue> {
248 self.body.as_ref()
249 }
250
251 #[must_use]
252 pub const fn query(&self) -> Option<&JsValue> {
253 self.query.as_ref()
254 }
255
256 #[must_use]
258 pub fn with_params_value(mut self, params: JsValue) -> Self {
259 self.params = Some(params);
260 self
261 }
262
263 #[must_use]
264 pub const fn params(&self) -> Option<&JsValue> {
265 self.params.as_ref()
266 }
267
268 #[must_use]
269 pub const fn headers(&self) -> Option<&HashMap<String, String>> {
270 self.headers.as_ref()
271 }
272
273 pub fn replace_headers(&mut self, headers: HashMap<String, String>) {
276 self.headers = Some(headers);
277 }
278
279 #[must_use]
280 pub const fn request(&self) -> Option<&AuthRequest> {
281 self.request.as_ref()
282 }
283
284 #[must_use]
285 pub const fn phase(&self) -> EndpointPhase {
286 self.phase
287 }
288
289 #[must_use]
290 pub fn has_body(&self) -> bool {
291 self.phase != EndpointPhase::Raw || self.body.is_some()
292 }
293
294 #[must_use]
295 pub fn has_query(&self) -> bool {
296 self.phase != EndpointPhase::Raw || self.query.is_some()
297 }
298
299 #[must_use]
300 pub fn has_method(&self) -> bool {
301 self.phase != EndpointPhase::Raw || self.method.is_some()
302 }
303
304 #[must_use]
306 pub fn middleware_context(&self) -> Self {
307 let mut context = self.clone();
308 context.phase = EndpointPhase::Middleware;
309 if context.path.as_ref().is_none_or(String::is_empty) {
310 context.path = Some("/".into());
311 }
312 context
313 }
314
315 #[must_use]
317 pub fn handler_context(&self, body: Option<JsValue>, query: Option<JsValue>) -> Self {
318 let mut context = self.clone();
319 context.phase = EndpointPhase::Handler;
320 if context.path.as_ref().is_none_or(String::is_empty) {
321 context.path = Some("virtual:".into());
322 }
323 if context.method.is_none() {
324 context.method = Some(context.default_method.clone());
325 }
326 context.body = body;
327 context.query = query;
328 context
329 }
330
331 pub(crate) fn session_read_context(&self) -> Self {
333 let mut context = self.clone();
334 context.phase = EndpointPhase::Handler;
335 if context.path.as_ref().is_none_or(String::is_empty) {
336 context.path = Some("virtual:".into());
337 }
338 context.method = Some(HttpMethod::Get);
339 let mut query = indexmap::IndexMap::new();
340 for name in ["disableCookieCache", "disableRefresh"] {
341 if self
342 .query
343 .as_ref()
344 .and_then(|value| value.get(name))
345 .is_some()
346 {
347 _ = query.insert(name.into(), JsValue::Bool(self.session_query_truthy(name)));
348 }
349 }
350 context.query = Some(JsValue::Object(query));
351 context
352 }
353
354 #[must_use]
355 pub const fn extensions(&self) -> &RequestExtensions {
356 &self.extensions
357 }
358
359 #[must_use]
360 pub const fn default_method(&self) -> &HttpMethod {
361 &self.default_method
362 }
363
364 #[must_use]
365 pub fn virtual_session(&self) -> Option<SessionView> {
366 self.state.lock_unpoisoned().session.clone()
367 }
368
369 pub fn establish_session<S: AuthSchema>(
372 &self,
373 user: S::User,
374 user_view: UserView,
375 session: SessionView,
376 context: &AuthContext<S>,
377 ) {
378 self.extensions.insert(VerifiedEndpointUser::<S> {
379 user,
380 config: context.config.clone(),
381 database: context.database.clone(),
382 });
383 let mut state = self.state.lock_unpoisoned();
384 state.principal = Some((user_view, session.clone()));
385 state.session = Some(session);
386 }
387
388 #[must_use]
390 pub fn session(&self) -> Option<(UserView, SessionView)> {
391 self.state.lock_unpoisoned().principal.clone()
392 }
393
394 pub(crate) fn clear_authenticated_session(&self) {
395 let mut state = self.state.lock_unpoisoned();
396 state.session = None;
397 state.principal = None;
398 }
399
400 pub fn record_authenticated_session(&self, user: UserView, session: SessionView) {
401 self.state.lock_unpoisoned().principal = Some((user, session));
402 }
403
404 #[must_use]
405 pub fn authenticated_user<S: AuthSchema>(&self, context: &AuthContext<S>) -> Option<S::User> {
406 self.extensions
407 .get::<VerifiedEndpointUser<S>>()
408 .filter(|user| {
409 Arc::ptr_eq(&user.config, &context.config)
410 && Arc::ptr_eq(&user.database, &context.database)
411 })
412 .map(|user| user.user.clone())
413 }
414
415 pub fn set_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
416 _ = self
417 .state
418 .lock_unpoisoned()
419 .response_headers
420 .insert(name, value);
421 }
422
423 pub fn queue_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
424 self.state
425 .lock_unpoisoned()
426 .response_headers
427 .append(name, value);
428 }
429
430 pub fn take_response_headers(&self) -> Headers {
431 std::mem::take(&mut self.state.lock_unpoisoned().response_headers)
432 }
433
434 pub fn set_session_hook_snapshot(&self, user: UserView, session: SessionView) {
435 self.state.lock_unpoisoned().session_observation = Some((user, session));
436 }
437
438 #[must_use]
439 pub fn session_hook_snapshot(&self) -> Option<(UserView, SessionView)> {
440 self.state.lock_unpoisoned().session_observation.clone()
441 }
442
443 pub fn body_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
446 Ok(crate::utils::json::from_value(
447 self.body.clone().unwrap_or(JsValue::Null),
448 )?)
449 }
450
451 pub fn query_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
454 Ok(crate::utils::json::from_value(
455 self.query.clone().unwrap_or(JsValue::Null),
456 )?)
457 }
458}
459
460#[derive(Clone, Debug, Default)]
462pub struct EndpointContextPatch {
463 pub method: Option<HttpMethod>,
464 pub request: Option<AuthRequest>,
465 pub path: Option<String>,
466 pub body: Option<JsValue>,
467 pub query: Option<JsValue>,
468 pub params: Option<JsValue>,
469 pub headers: Option<HashMap<String, String>>,
470}
471
472impl EndpointContextPatch {
473 pub fn merge(&mut self, patch: Self) {
474 if let Some(method) = patch.method {
475 self.method = Some(method);
476 }
477 if let Some(request) = patch.request {
478 self.request = Some(request);
479 }
480 if let Some(path) = patch.path {
481 self.path = Some(path);
482 }
483 merge_optional(&mut self.body, patch.body);
484 merge_optional(&mut self.query, patch.query);
485 merge_optional(&mut self.params, patch.params);
486 if let Some(headers) = patch.headers {
487 self.headers.get_or_insert_with(HashMap::new).extend(
488 headers
489 .into_iter()
490 .map(|(name, value)| (name.to_ascii_lowercase(), value)),
491 );
492 }
493 }
494
495 pub fn apply(self, call: &mut EndpointCall) {
496 if let Some(method) = self.method {
497 call.method = Some(method);
498 }
499 if let Some(request) = self.request {
500 call.request = Some(fresh_request(request));
501 }
502 if let Some(path) = self.path {
503 call.path = Some(path);
504 }
505 merge_optional(&mut call.body, self.body);
506 merge_optional(&mut call.query, self.query);
507 merge_optional(&mut call.params, self.params);
508 if let Some(headers) = self.headers {
509 call.headers
510 .get_or_insert_with(HashMap::new)
511 .extend(headers);
512 }
513 }
514}
515
516fn merge_optional(target: &mut Option<JsValue>, patch: Option<JsValue>) {
517 if let Some(patch) = patch.filter(|value| !value.is_null()) {
518 if let Some(target) = target {
519 merge_value(target, patch);
520 } else {
521 *target = Some(patch);
522 }
523 }
524}
525
526fn merge_value(target: &mut JsValue, patch: JsValue) {
527 if let (JsValue::Object(target), JsValue::Object(patch)) = (&mut *target, &patch) {
528 for (name, value) in patch {
529 if value.is_null() || matches!(name.as_str(), "__proto__" | "constructor") {
530 continue;
531 }
532 if let Some(target) = target.get_mut(name) {
533 merge_value(target, value.clone());
534 } else {
535 _ = target.insert(name.clone(), value.clone());
536 }
537 }
538 } else {
539 *target = patch;
540 }
541}
542
543#[derive(Debug)]
545pub enum BeforeEndpointAction {
546 Patch(Box<EndpointContextPatch>),
547 Respond(EndpointResponse),
548 Reject(EndpointResponse),
550}
551
552#[derive(Clone, Debug, Default)]
554pub struct EndpointInput {
555 pub body: Option<JsValue>,
556 pub query: Option<JsValue>,
557}
558
559#[derive(Debug)]
561pub struct EndpointResponse {
562 result: Result<JsValue, AuthError>,
563 http_response: Option<crate::AuthResponse>,
564 error_body: Option<JsValue>,
565 error_body_absent: bool,
566 headers: Headers,
567 status: Option<u16>,
568}
569
570impl EndpointResponse {
571 #[must_use]
572 pub fn value(value: JsValue) -> Self {
573 Self {
574 result: Ok(value),
575 http_response: None,
576 error_body: None,
577 error_body_absent: false,
578 headers: Headers::new(),
579 status: None,
580 }
581 }
582
583 pub fn json(value: &impl Serialize) -> AuthResult<Self> {
586 Ok(Self::value(crate::utils::json::parse_value(
587 &crate::utils::json::to_string(value)?,
588 )?))
589 }
590
591 #[must_use]
593 pub fn empty() -> Self {
594 let mut response = Self::value(JsValue::Null);
595 response.http_response = Some(crate::AuthResponse::new(200));
596 response
597 }
598
599 #[must_use]
604 pub fn from_http_response(response: crate::AuthResponse) -> Self {
605 let mut result = Self::value(JsValue::Object(indexmap::IndexMap::new()));
606 result.http_response = Some(response);
607 result
608 }
609
610 #[must_use]
611 pub const fn http_response(&self) -> Option<&crate::AuthResponse> {
612 self.http_response.as_ref()
613 }
614
615 pub fn http_response_mut(&mut self) -> Option<&mut crate::AuthResponse> {
616 self.http_response.as_mut()
617 }
618
619 pub fn to_response(&self) -> AuthResult<crate::AuthResponse> {
624 let response = match &self.result {
625 Ok(value) => match &self.http_response {
626 Some(response) => response.clone(),
627 None => crate::AuthResponse::json(self.status.unwrap_or(200), value)?,
628 },
629 Err(error) => {
630 let (status, code, message) = error.error_payload();
631 let mut response = crate::AuthResponse::json(
632 self.status.unwrap_or(status),
633 &crate::ErrorCodeMessageResponse { code, message },
634 )?;
635 if self.error_body_absent {
636 response.body.clear();
637 } else if let Some(body) = &self.error_body {
638 response.body = crate::utils::json::to_vec(body)?;
639 }
640 response
641 }
642 };
643 Ok(forward_response_headers(response, &self.headers))
644 }
645
646 #[must_use]
647 pub fn error(error: AuthError) -> Self {
648 let status = Some(error.status_code());
649 Self {
650 result: Err(error),
651 http_response: None,
652 error_body: None,
653 error_body_absent: false,
654 headers: Headers::new(),
655 status,
656 }
657 }
658
659 #[must_use]
661 pub fn redirect(location: impl Into<String>) -> Self {
662 Self::error(AuthError::Api {
663 status: 302,
664 code: None,
665 message: String::new(),
666 })
667 .without_error_body()
668 .with_header("location", location)
669 }
670
671 #[must_use]
673 pub fn without_error_body(mut self) -> Self {
674 self.error_body = None;
675 self.error_body_absent = true;
676 self
677 }
678
679 #[must_use]
680 pub const fn error_body_is_absent(&self) -> bool {
681 self.error_body_absent
682 }
683
684 #[must_use]
685 pub fn with_error_body(mut self, body: JsValue) -> Self {
686 self.error_body_absent = false;
687 self.error_body = Some(body);
688 self
689 }
690
691 #[must_use]
692 pub const fn error_body(&self) -> Option<&JsValue> {
693 self.error_body.as_ref()
694 }
695
696 pub const fn result(&self) -> &Result<JsValue, AuthError> {
697 &self.result
698 }
699
700 #[must_use]
701 pub const fn headers(&self) -> &Headers {
702 &self.headers
703 }
704
705 #[must_use]
706 pub const fn status(&self) -> Option<u16> {
707 self.status
708 }
709
710 pub fn set_status(&mut self, status: Option<u16>) {
711 self.status = status;
712 }
713
714 #[must_use]
715 pub fn with_status(mut self, status: u16) -> Self {
716 self.status = Some(status);
717 self
718 }
719
720 #[must_use]
721 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
722 merge_header(&mut self.headers, name.into(), value.into());
723 self
724 }
725
726 pub fn merge_headers(&mut self, headers: Headers) {
727 for (name, value) in headers {
728 merge_header(&mut self.headers, name, value);
729 }
730 }
731
732 pub fn replace(&mut self, value: JsValue) {
734 self.result = Ok(value);
735 self.http_response = None;
736 self.error_body = None;
737 self.error_body_absent = false;
738 }
739
740 pub fn replace_error(&mut self, error: AuthError) {
741 self.result = Err(error);
742 self.http_response = None;
743 self.error_body = None;
744 self.error_body_absent = false;
745 }
746
747 pub fn into_output<T>(self) -> Result<EndpointOutput<T>, EndpointError> {
748 match self.result {
749 Ok(value) => Ok(EndpointOutput {
750 value,
751 http_response: self.http_response,
752 headers: self.headers,
753 status: self.status,
754 output: PhantomData,
755 }),
756 Err(error) => Err(EndpointError {
757 error,
758 headers: Some(self.headers),
759 body: self.error_body.map(Box::new),
760 body_absent: self.error_body_absent,
761 }),
762 }
763 }
764}
765
766fn merge_header(headers: &mut Headers, name: String, value: String) {
767 if name.eq_ignore_ascii_case("set-cookie") {
768 headers.append(name, value);
769 } else {
770 _ = headers.insert(name, value);
771 }
772}
773
774#[async_trait::async_trait]
776pub trait EndpointHook<S: AuthSchema>: Send + Sync {
777 fn matches_before(&self, _call: &EndpointCall, _ctx: &AuthContext<S>) -> AuthResult<bool> {
781 Ok(true)
782 }
783
784 async fn before(
787 &self,
788 _call: &EndpointCall,
789 _ctx: &AuthContext<S>,
790 ) -> AuthResult<Option<BeforeEndpointAction>> {
791 Ok(None)
792 }
793
794 fn matches_after(
797 &self,
798 _call: &EndpointCall,
799 _ctx: &AuthContext<S>,
800 _response: &EndpointResponse,
801 ) -> AuthResult<bool> {
802 Ok(true)
803 }
804
805 async fn after(
808 &self,
809 _call: &EndpointCall,
810 _ctx: &AuthContext<S>,
811 response: EndpointResponse,
812 ) -> AuthResult<EndpointResponse> {
813 Ok(response)
814 }
815}
816
817#[derive(Debug)]
819pub struct EndpointOutput<T> {
820 value: JsValue,
821 http_response: Option<crate::AuthResponse>,
822 headers: Headers,
823 status: Option<u16>,
824 output: PhantomData<fn() -> T>,
825}
826
827impl<T> EndpointOutput<T> {
828 #[must_use]
830 pub const fn http_response(&self) -> Option<&crate::AuthResponse> {
831 self.http_response.as_ref()
832 }
833
834 pub fn into_response(self) -> AuthResult<crate::AuthResponse> {
842 let response = match self.http_response {
843 Some(response) => response,
844 None => crate::AuthResponse::json(self.status.unwrap_or(200), &self.value)?,
845 };
846 Ok(forward_response_headers(response, &self.headers))
847 }
848 #[must_use]
849 pub const fn value(&self) -> &JsValue {
850 &self.value
851 }
852
853 #[must_use]
854 pub const fn headers(&self) -> &Headers {
855 &self.headers
856 }
857
858 #[must_use]
859 pub const fn status(&self) -> Option<u16> {
860 self.status
861 }
862}
863
864impl<T: DeserializeOwned + 'static> EndpointOutput<T> {
865 pub fn decode(&self) -> AuthResult<T> {
868 if self
869 .http_response
870 .as_ref()
871 .is_some_and(|response| !(response.body.is_empty() && self.value == JsValue::Null))
872 {
873 return Err(AuthError::internal(
874 "Endpoint returned an HTTP response; use http_response or into_response",
875 ));
876 }
877 Ok(crate::utils::json::from_value(self.value.clone())?)
878 }
879}
880
881fn forward_response_headers(
882 mut response: crate::AuthResponse,
883 headers: &Headers,
884) -> crate::AuthResponse {
885 for (name, value) in headers {
886 if matches!(
889 name.to_ascii_lowercase().as_str(),
890 "host"
891 | "user-agent"
892 | "referer"
893 | "from"
894 | "expect"
895 | "authorization"
896 | "proxy-authorization"
897 | "cookie"
898 | "origin"
899 | "accept-charset"
900 | "accept-encoding"
901 | "accept-language"
902 | "if-match"
903 | "if-none-match"
904 | "if-modified-since"
905 | "if-unmodified-since"
906 | "if-range"
907 | "range"
908 | "max-forwards"
909 | "connection"
910 | "keep-alive"
911 | "transfer-encoding"
912 | "te"
913 | "upgrade"
914 | "trailer"
915 | "proxy-connection"
916 | "content-length"
917 ) {
918 continue;
919 }
920 merge_header(&mut response.headers, name.clone(), value.clone());
921 }
922 response
923}
924
925#[derive(Debug)]
927pub struct EndpointError {
928 pub error: AuthError,
929 pub headers: Option<Headers>,
930 pub body: Option<Box<JsValue>>,
931 pub body_absent: bool,
933}
934
935impl From<AuthError> for EndpointError {
936 fn from(error: AuthError) -> Self {
937 Self {
938 error,
939 headers: None,
940 body: None,
941 body_absent: false,
942 }
943 }
944}
945
946impl std::fmt::Display for EndpointError {
947 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
948 self.error.fmt(formatter)
949 }
950}
951
952impl std::error::Error for EndpointError {
953 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
954 Some(&self.error)
955 }
956}
957
958#[must_use]
960pub fn is_endpoint_api_error(error: &AuthError) -> bool {
961 matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) || error.status_code() < 500
962}
963
964fn fresh_request(request: AuthRequest) -> AuthRequest {
965 let pairs = request
966 .query
967 .keys()
968 .flat_map(|name| {
969 request
970 .query_values(name)
971 .into_iter()
972 .flatten()
973 .map(|value| (name.clone(), value.clone()))
974 })
975 .collect::<Vec<_>>();
976 let url = request.url().cloned();
977 let mut fresh = AuthRequest::from_parts(
978 request.method,
979 request.path,
980 request.headers,
981 request.body,
982 request.query,
983 );
984 if let Some(url) = url {
985 fresh = fresh.with_url(url);
986 }
987 fresh.set_query_pairs(pairs);
988 fresh
989}
990
991tokio::task_local! { static ENDPOINT_CALL_CONTEXT: Option<EndpointCall>; }
992
993#[must_use]
994pub fn current_endpoint_call_context() -> Option<EndpointCall> {
995 ENDPOINT_CALL_CONTEXT.try_with(Clone::clone).ok().flatten()
996}
997
998pub async fn with_endpoint_call_context<T>(
999 call: EndpointCall,
1000 future: impl Future<Output = T>,
1001) -> T {
1002 let request = call
1003 .request()
1004 .map(crate::hooks::RequestHookContext::from_request);
1005 crate::hooks::with_optional_request_hook_context(
1006 request,
1007 ENDPOINT_CALL_CONTEXT.scope(Some(call), future),
1008 )
1009 .await
1010}
1011
1012pub async fn without_endpoint_call_context<T>(future: impl Future<Output = T>) -> T {
1014 ENDPOINT_CALL_CONTEXT.scope(None, future).await
1015}