1use crate::types::RequestExtensions;
4use crate::utils::json::JsValue;
5use crate::wire::{SessionView, UserView};
6use crate::{AuthError, AuthRequest, AuthResult, AuthSchema, Headers, HttpMethod};
7use serde::{Serialize, de::DeserializeOwned};
8use std::collections::HashMap;
9use std::marker::PhantomData;
10use std::sync::{Arc, Mutex};
11
12#[derive(Clone, Debug)]
14pub struct EndpointDefinition {
15 pub name: &'static str,
16 pub operation_id: &'static str,
17 pub path: Option<String>,
18 pub method: HttpMethod,
19}
20
21#[derive(Clone, Debug)]
23pub struct ServerEndpoint<T> {
24 plugin: &'static str,
25 name: &'static str,
26 body: Option<JsValue>,
27 query: Option<JsValue>,
28 output: PhantomData<fn() -> T>,
29}
30
31impl<T> ServerEndpoint<T> {
32 #[must_use]
33 pub const fn new(plugin: &'static str, name: &'static str) -> Self {
34 Self {
35 plugin,
36 name,
37 body: None,
38 query: None,
39 output: PhantomData,
40 }
41 }
42
43 pub fn with_body(mut self, body: &impl Serialize) -> AuthResult<Self> {
46 self.body = Some(crate::utils::json::parse_value(
47 &crate::utils::json::to_string(body)?,
48 )?);
49 Ok(self)
50 }
51
52 pub fn with_query(mut self, query: &impl Serialize) -> AuthResult<Self> {
55 self.query = Some(crate::utils::json::parse_value(
56 &crate::utils::json::to_string(query)?,
57 )?);
58 Ok(self)
59 }
60
61 #[must_use]
62 pub fn with_body_value(mut self, body: JsValue) -> Self {
63 self.body = Some(body);
64 self
65 }
66
67 #[must_use]
68 pub fn with_query_value(mut self, query: JsValue) -> Self {
69 self.query = Some(query);
70 self
71 }
72
73 #[must_use]
74 pub const fn plugin(&self) -> &'static str {
75 self.plugin
76 }
77
78 #[must_use]
79 pub const fn name(&self) -> &'static str {
80 self.name
81 }
82
83 #[must_use]
84 pub fn input(self) -> (Option<JsValue>, Option<JsValue>) {
85 (self.body, self.query)
86 }
87}
88
89#[derive(Clone, Debug, Default)]
91pub struct EndpointOptions {
92 pub headers: Option<HashMap<String, String>>,
93 pub request: Option<AuthRequest>,
94 pub method: Option<HttpMethod>,
95}
96
97#[derive(Clone, Debug, Default)]
98struct EndpointState {
99 session: Option<SessionView>,
100 principal: Option<(UserView, SessionView)>,
101 response_headers: Headers,
102 session_observation: Option<(UserView, SessionView)>,
103}
104
105struct VerifiedEndpointUser<S: AuthSchema> {
106 user: S::User,
107 config: Arc<crate::AuthConfig>,
108 database: Arc<dyn crate::AuthStore<S>>,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum EndpointPhase {
114 Raw,
115 Middleware,
116 Handler,
117}
118
119#[derive(Clone, Debug)]
125pub struct EndpointCall {
126 operation_id: String,
127 path: Option<String>,
128 method: Option<HttpMethod>,
129 body: Option<JsValue>,
130 query: Option<JsValue>,
131 headers: Option<HashMap<String, String>>,
132 request: Option<AuthRequest>,
133 default_method: HttpMethod,
134 phase: EndpointPhase,
135 extensions: RequestExtensions,
136 state: Arc<Mutex<EndpointState>>,
137}
138
139impl EndpointCall {
140 #[must_use]
141 pub fn new(
142 definition: &EndpointDefinition,
143 body: Option<JsValue>,
144 query: Option<JsValue>,
145 options: EndpointOptions,
146 ) -> Self {
147 Self {
148 operation_id: definition.operation_id.into(),
149 path: definition.path.clone(),
150 method: options.method,
151 body,
152 query,
153 headers: options.headers.map(|headers| {
154 headers
155 .into_iter()
156 .map(|(name, value)| (name.to_ascii_lowercase(), value))
157 .collect()
158 }),
159 request: options.request.map(fresh_request),
160 default_method: definition.method.clone(),
161 phase: EndpointPhase::Raw,
162 extensions: RequestExtensions::default(),
163 state: Arc::new(Mutex::new(EndpointState::default())),
164 }
165 }
166
167 #[must_use]
170 pub fn from_http_request(request: &AuthRequest, route: &crate::AuthRoute) -> Self {
171 let body = request
172 .extensions()
173 .get::<crate::types::ParsedRequestBody>()
174 .and_then(|body| match body.as_ref() {
175 crate::types::ParsedRequestBody::Value(value) => Some(value.clone()),
176 crate::types::ParsedRequestBody::Opaque(_) => None,
177 });
178 let query = JsValue::Object(
179 request
180 .query
181 .keys()
182 .map(|name| {
183 let values = request.query_values(name).unwrap_or_default();
184 let value = if values.len() > 1 {
185 JsValue::Array(values.iter().cloned().map(JsValue::String).collect())
186 } else {
187 JsValue::String(request.query.get(name).cloned().unwrap_or_default())
188 };
189 (name.clone(), value)
190 })
191 .collect(),
192 );
193 let definition = EndpointDefinition {
194 name: "http",
195 operation_id: "http",
196 path: Some(route.context_path.as_ref().unwrap_or(&route.path).clone()),
197 method: route.method.clone(),
198 };
199 let mut call = Self::new(
200 &definition,
201 body,
202 Some(query),
203 EndpointOptions {
204 headers: Some(request.headers.clone()),
205 request: Some(
206 crate::hooks::current_request_hook_context()
207 .map_or_else(|| request.clone(), |context| context.request),
208 ),
209 method: Some(request.method.clone()),
210 },
211 );
212 call.operation_id.clone_from(&route.operation_id);
213 call
214 }
215
216 #[must_use]
217 pub fn operation_id(&self) -> &str {
218 &self.operation_id
219 }
220
221 #[must_use]
222 pub fn path(&self) -> Option<&str> {
223 self.path.as_deref()
224 }
225
226 #[must_use]
227 pub const fn method(&self) -> Option<&HttpMethod> {
228 self.method.as_ref()
229 }
230
231 #[must_use]
232 pub const fn body(&self) -> Option<&JsValue> {
233 self.body.as_ref()
234 }
235
236 #[must_use]
237 pub const fn query(&self) -> Option<&JsValue> {
238 self.query.as_ref()
239 }
240
241 #[must_use]
242 pub const fn headers(&self) -> Option<&HashMap<String, String>> {
243 self.headers.as_ref()
244 }
245
246 pub fn replace_headers(&mut self, headers: HashMap<String, String>) {
249 self.headers = Some(headers);
250 }
251
252 #[must_use]
253 pub const fn request(&self) -> Option<&AuthRequest> {
254 self.request.as_ref()
255 }
256
257 #[must_use]
258 pub const fn phase(&self) -> EndpointPhase {
259 self.phase
260 }
261
262 #[must_use]
263 pub fn has_body(&self) -> bool {
264 self.phase != EndpointPhase::Raw || self.body.is_some()
265 }
266
267 #[must_use]
268 pub fn has_query(&self) -> bool {
269 self.phase != EndpointPhase::Raw || self.query.is_some()
270 }
271
272 #[must_use]
273 pub fn has_method(&self) -> bool {
274 self.phase != EndpointPhase::Raw || self.method.is_some()
275 }
276
277 #[must_use]
279 pub fn middleware_context(&self) -> Self {
280 let mut context = self.clone();
281 context.phase = EndpointPhase::Middleware;
282 if context.path.as_ref().is_none_or(String::is_empty) {
283 context.path = Some("/".into());
284 }
285 context
286 }
287
288 #[must_use]
290 pub fn handler_context(&self, body: Option<JsValue>, query: Option<JsValue>) -> Self {
291 let mut context = self.clone();
292 context.phase = EndpointPhase::Handler;
293 if context.path.as_ref().is_none_or(String::is_empty) {
294 context.path = Some("virtual:".into());
295 }
296 if context.method.is_none() {
297 context.method = Some(context.default_method.clone());
298 }
299 context.body = body;
300 context.query = query;
301 context
302 }
303
304 pub(crate) fn session_read_context(&self) -> Self {
306 use crate::session::SessionRequest;
307 let mut context = self.clone();
308 context.phase = EndpointPhase::Handler;
309 if context.path.as_ref().is_none_or(String::is_empty) {
310 context.path = Some("virtual:".into());
311 }
312 context.method = Some(HttpMethod::Get);
313 let mut query = indexmap::IndexMap::new();
314 for name in ["disableCookieCache", "disableRefresh"] {
315 if self
316 .query
317 .as_ref()
318 .and_then(|value| value.get(name))
319 .is_some()
320 {
321 drop(query.insert(name.into(), JsValue::Bool(self.session_query_truthy(name))));
322 }
323 }
324 context.query = Some(JsValue::Object(query));
325 context
326 }
327
328 #[must_use]
329 pub const fn extensions(&self) -> &RequestExtensions {
330 &self.extensions
331 }
332
333 #[must_use]
334 pub const fn default_method(&self) -> &HttpMethod {
335 &self.default_method
336 }
337
338 #[must_use]
339 pub fn virtual_session(&self) -> Option<SessionView> {
340 self.state
341 .lock()
342 .unwrap_or_else(std::sync::PoisonError::into_inner)
343 .session
344 .clone()
345 }
346
347 pub fn establish_session<S: AuthSchema>(
350 &self,
351 user: S::User,
352 user_view: UserView,
353 session: SessionView,
354 context: &crate::AuthContext<S>,
355 ) {
356 self.extensions.insert(VerifiedEndpointUser::<S> {
357 user,
358 config: context.config.clone(),
359 database: context.database.clone(),
360 });
361 let mut state = self
362 .state
363 .lock()
364 .unwrap_or_else(std::sync::PoisonError::into_inner);
365 state.principal = Some((user_view, session.clone()));
366 state.session = Some(session);
367 }
368
369 #[must_use]
371 pub fn session(&self) -> Option<(UserView, SessionView)> {
372 self.state
373 .lock()
374 .unwrap_or_else(std::sync::PoisonError::into_inner)
375 .principal
376 .clone()
377 }
378
379 pub fn record_authenticated_session(&self, user: UserView, session: SessionView) {
380 self.state
381 .lock()
382 .unwrap_or_else(std::sync::PoisonError::into_inner)
383 .principal = Some((user, session));
384 }
385
386 #[must_use]
387 pub fn authenticated_user<S: AuthSchema>(
388 &self,
389 context: &crate::AuthContext<S>,
390 ) -> Option<S::User> {
391 self.extensions
392 .get::<VerifiedEndpointUser<S>>()
393 .filter(|user| {
394 Arc::ptr_eq(&user.config, &context.config)
395 && Arc::ptr_eq(&user.database, &context.database)
396 })
397 .map(|user| user.user.clone())
398 }
399
400 pub fn set_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
401 drop(
402 self.state
403 .lock()
404 .unwrap_or_else(std::sync::PoisonError::into_inner)
405 .response_headers
406 .insert(name, value),
407 );
408 }
409
410 pub fn queue_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
411 self.state
412 .lock()
413 .unwrap_or_else(std::sync::PoisonError::into_inner)
414 .response_headers
415 .append(name, value);
416 }
417
418 pub fn take_response_headers(&self) -> Headers {
419 std::mem::take(
420 &mut self
421 .state
422 .lock()
423 .unwrap_or_else(std::sync::PoisonError::into_inner)
424 .response_headers,
425 )
426 }
427
428 pub fn set_session_hook_snapshot(&self, user: UserView, session: SessionView) {
429 self.state
430 .lock()
431 .unwrap_or_else(std::sync::PoisonError::into_inner)
432 .session_observation = Some((user, session));
433 }
434
435 #[must_use]
436 pub fn session_hook_snapshot(&self) -> Option<(UserView, SessionView)> {
437 self.state
438 .lock()
439 .unwrap_or_else(std::sync::PoisonError::into_inner)
440 .session_observation
441 .clone()
442 }
443
444 pub fn body_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
447 Ok(crate::utils::json::from_value(
448 self.body.clone().unwrap_or(JsValue::Null),
449 )?)
450 }
451
452 pub fn query_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
455 Ok(crate::utils::json::from_value(
456 self.query.clone().unwrap_or(JsValue::Null),
457 )?)
458 }
459}
460
461#[derive(Clone, Debug, Default)]
463pub struct EndpointContextPatch {
464 pub method: Option<HttpMethod>,
465 pub request: Option<AuthRequest>,
466 pub path: Option<String>,
467 pub body: Option<JsValue>,
468 pub query: 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 if let Some(headers) = patch.headers {
486 self.headers.get_or_insert_with(HashMap::new).extend(
487 headers
488 .into_iter()
489 .map(|(name, value)| (name.to_ascii_lowercase(), value)),
490 );
491 }
492 }
493
494 pub fn apply(self, call: &mut EndpointCall) {
495 if let Some(method) = self.method {
496 call.method = Some(method);
497 }
498 if let Some(request) = self.request {
499 call.request = Some(fresh_request(request));
500 }
501 if let Some(path) = self.path {
502 call.path = Some(path);
503 }
504 merge_optional(&mut call.body, self.body);
505 merge_optional(&mut call.query, self.query);
506 if let Some(headers) = self.headers {
507 call.headers
508 .get_or_insert_with(HashMap::new)
509 .extend(headers);
510 }
511 }
512}
513
514fn merge_optional(target: &mut Option<JsValue>, patch: Option<JsValue>) {
515 if let Some(patch) = patch.filter(|value| !value.is_null()) {
516 if let Some(target) = target {
517 merge_value(target, patch);
518 } else {
519 *target = Some(patch);
520 }
521 }
522}
523
524fn merge_value(target: &mut JsValue, patch: JsValue) {
525 if let (JsValue::Object(target), JsValue::Object(patch)) = (&mut *target, &patch) {
526 for (name, value) in patch {
527 if value.is_null() || matches!(name.as_str(), "__proto__" | "constructor") {
528 continue;
529 }
530 if let Some(target) = target.get_mut(name) {
531 merge_value(target, value.clone());
532 } else {
533 drop(target.insert(name.clone(), value.clone()));
534 }
535 }
536 } else {
537 *target = patch;
538 }
539}
540
541#[derive(Debug)]
543pub enum BeforeEndpointAction {
544 Patch(Box<EndpointContextPatch>),
545 Respond(EndpointResponse),
546 Reject(EndpointResponse),
548}
549
550#[derive(Clone, Debug, Default)]
552pub struct EndpointInput {
553 pub body: Option<JsValue>,
554 pub query: Option<JsValue>,
555}
556
557#[derive(Debug)]
559pub struct EndpointResponse {
560 result: Result<JsValue, AuthError>,
561 error_body: Option<JsValue>,
562 headers: Headers,
563 status: Option<u16>,
564}
565
566impl EndpointResponse {
567 #[must_use]
568 pub fn value(value: JsValue) -> Self {
569 Self {
570 result: Ok(value),
571 error_body: None,
572 headers: Headers::new(),
573 status: None,
574 }
575 }
576
577 pub fn json(value: &impl Serialize) -> AuthResult<Self> {
580 Ok(Self::value(crate::utils::json::parse_value(
581 &crate::utils::json::to_string(value)?,
582 )?))
583 }
584
585 #[must_use]
586 pub fn error(error: AuthError) -> Self {
587 let status = Some(error.status_code());
588 Self {
589 result: Err(error),
590 error_body: None,
591 headers: Headers::new(),
592 status,
593 }
594 }
595
596 #[must_use]
597 pub fn with_error_body(mut self, body: JsValue) -> Self {
598 self.error_body = Some(body);
599 self
600 }
601
602 #[must_use]
603 pub const fn error_body(&self) -> Option<&JsValue> {
604 self.error_body.as_ref()
605 }
606
607 pub const fn result(&self) -> &Result<JsValue, AuthError> {
608 &self.result
609 }
610
611 #[must_use]
612 pub const fn headers(&self) -> &Headers {
613 &self.headers
614 }
615
616 #[must_use]
617 pub const fn status(&self) -> Option<u16> {
618 self.status
619 }
620
621 pub fn set_status(&mut self, status: Option<u16>) {
622 self.status = status;
623 }
624
625 #[must_use]
626 pub fn with_status(mut self, status: u16) -> Self {
627 self.status = Some(status);
628 self
629 }
630
631 #[must_use]
632 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
633 merge_header(&mut self.headers, name.into(), value.into());
634 self
635 }
636
637 pub fn merge_headers(&mut self, headers: Headers) {
638 for (name, value) in headers {
639 merge_header(&mut self.headers, name, value);
640 }
641 }
642
643 pub fn replace(&mut self, value: JsValue) {
645 self.result = Ok(value);
646 self.error_body = None;
647 }
648
649 pub fn replace_error(&mut self, error: AuthError) {
650 self.result = Err(error);
651 self.error_body = None;
652 }
653
654 pub fn into_output<T>(self) -> Result<EndpointOutput<T>, EndpointError> {
655 match self.result {
656 Ok(value) => Ok(EndpointOutput {
657 value,
658 headers: self.headers,
659 status: self.status,
660 output: PhantomData,
661 }),
662 Err(error) => Err(EndpointError {
663 error,
664 headers: Some(self.headers),
665 body: self.error_body.map(Box::new),
666 }),
667 }
668 }
669}
670
671fn merge_header(headers: &mut Headers, name: String, value: String) {
672 if name.eq_ignore_ascii_case("set-cookie") {
673 headers.append(name, value);
674 } else {
675 drop(headers.insert(name, value));
676 }
677}
678
679#[async_trait::async_trait]
681pub trait EndpointHook<S: AuthSchema>: Send + Sync {
682 fn matches_before(
686 &self,
687 _call: &EndpointCall,
688 _ctx: &crate::AuthContext<S>,
689 ) -> AuthResult<bool> {
690 Ok(true)
691 }
692
693 async fn before(
696 &self,
697 _call: &EndpointCall,
698 _ctx: &crate::AuthContext<S>,
699 ) -> AuthResult<Option<BeforeEndpointAction>> {
700 Ok(None)
701 }
702
703 fn matches_after(
706 &self,
707 _call: &EndpointCall,
708 _ctx: &crate::AuthContext<S>,
709 _response: &EndpointResponse,
710 ) -> AuthResult<bool> {
711 Ok(true)
712 }
713
714 async fn after(
717 &self,
718 _call: &EndpointCall,
719 _ctx: &crate::AuthContext<S>,
720 response: EndpointResponse,
721 ) -> AuthResult<EndpointResponse> {
722 Ok(response)
723 }
724}
725
726#[derive(Debug)]
728pub struct EndpointOutput<T> {
729 value: JsValue,
730 headers: Headers,
731 status: Option<u16>,
732 output: PhantomData<fn() -> T>,
733}
734
735impl<T> EndpointOutput<T> {
736 #[must_use]
737 pub const fn value(&self) -> &JsValue {
738 &self.value
739 }
740
741 #[must_use]
742 pub const fn headers(&self) -> &Headers {
743 &self.headers
744 }
745
746 #[must_use]
747 pub const fn status(&self) -> Option<u16> {
748 self.status
749 }
750}
751
752impl<T: DeserializeOwned + 'static> EndpointOutput<T> {
753 pub fn decode(&self) -> AuthResult<T> {
756 Ok(crate::utils::json::from_value(self.value.clone())?)
757 }
758}
759
760#[derive(Debug)]
762pub struct EndpointError {
763 pub error: AuthError,
764 pub headers: Option<Headers>,
765 pub body: Option<Box<JsValue>>,
766}
767
768impl From<AuthError> for EndpointError {
769 fn from(error: AuthError) -> Self {
770 Self {
771 error,
772 headers: None,
773 body: None,
774 }
775 }
776}
777
778impl std::fmt::Display for EndpointError {
779 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780 self.error.fmt(formatter)
781 }
782}
783
784impl std::error::Error for EndpointError {
785 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
786 Some(&self.error)
787 }
788}
789
790#[must_use]
792pub fn is_endpoint_api_error(error: &AuthError) -> bool {
793 matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) || error.status_code() < 500
794}
795
796fn fresh_request(request: AuthRequest) -> AuthRequest {
797 let pairs = request
798 .query
799 .keys()
800 .flat_map(|name| {
801 request
802 .query_values(name)
803 .into_iter()
804 .flatten()
805 .map(|value| (name.clone(), value.clone()))
806 })
807 .collect::<Vec<_>>();
808 let url = request.url().cloned();
809 let mut fresh = AuthRequest::from_parts(
810 request.method,
811 request.path,
812 request.headers,
813 request.body,
814 request.query,
815 );
816 if let Some(url) = url {
817 fresh = fresh.with_url(url);
818 }
819 fresh.set_query_pairs(pairs);
820 fresh
821}
822
823tokio::task_local! { static ENDPOINT_CALL_CONTEXT: Option<EndpointCall>; }
824
825#[must_use]
826pub fn current_endpoint_call_context() -> Option<EndpointCall> {
827 ENDPOINT_CALL_CONTEXT.try_with(Clone::clone).ok().flatten()
828}
829
830pub async fn with_endpoint_call_context<T>(
831 call: EndpointCall,
832 future: impl Future<Output = T>,
833) -> T {
834 let request = call
835 .request()
836 .map(crate::hooks::RequestHookContext::from_request);
837 crate::hooks::with_optional_request_hook_context(
838 request,
839 ENDPOINT_CALL_CONTEXT.scope(Some(call), future),
840 )
841 .await
842}
843
844pub async fn without_endpoint_call_context<T>(future: impl Future<Output = T>) -> T {
846 ENDPOINT_CALL_CONTEXT.scope(None, future).await
847}