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