1use crate::hooks::{
3 RequestHookContext, update_request_hook_context, with_request_hook_context_value,
4};
5use crate::observability::EndpointHooks;
6use crate::{
7 AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
8 AuthSchema, BeforeRequestAction, HttpMethod,
9};
10use std::{future::Future, sync::Arc};
11
12pub struct EndpointDispatcher<S: AuthSchema> {
14 plugins: Arc<Vec<Box<dyn AuthPlugin<S>>>>,
15 hooks: EndpointHooks<S>,
16 routes: Vec<AuthRoute>,
17}
18impl<S: AuthSchema> EndpointDispatcher<S> {
19 pub fn new(
20 plugins: Arc<Vec<Box<dyn AuthPlugin<S>>>>,
21 hooks: EndpointHooks<S>,
22 core_routes: impl IntoIterator<Item = AuthRoute>,
23 ) -> Self {
24 let routes = core_routes
25 .into_iter()
26 .chain(plugins.iter().flat_map(|plugin| plugin.routes()))
27 .collect();
28 Self {
29 plugins,
30 hooks,
31 routes,
32 }
33 }
34
35 pub async fn native<F, Fut>(
37 &self,
38 mut request: AuthRequest,
39 route: AuthRoute,
40 context: &AuthContext<S>,
41 handler: F,
42 ) -> AuthResult<AuthResponse>
43 where
44 F: FnOnce(AuthRequest) -> Fut,
45 Fut: Future<Output = AuthResult<AuthResponse>>,
46 {
47 if !route.server_only {
48 return Err(AuthError::config(
49 "Native facade requires a server-only endpoint",
50 ));
51 }
52 request.path = "/".into();
53 request.set_server_only();
54 let mut scope = RequestHookContext::from_request(&request)?;
55 scope.path = None;
56 scope.body = request.input_field_value()?;
57 scope.operation_id = Some(route.operation_id.clone());
58 scope.meta = crate::RequestMeta::from_request_with_config(
59 &request,
60 &context.config.advanced.ip_address,
61 );
62 let method = match request.method() {
63 HttpMethod::Get => "GET",
64 HttpMethod::Post => "POST",
65 HttpMethod::Put => "PUT",
66 HttpMethod::Patch => "PATCH",
67 HttpMethod::Delete => "DELETE",
68 HttpMethod::Options => "OPTIONS",
69 HttpMethod::Head => "HEAD",
70 };
71 let name = format!("{method} /:virtual");
72 let operation_id = route.operation_id.clone();
73 with_request_hook_context_value(
74 scope,
75 crate::observability::with_span(
76 &context.config.experimental.instrumentation,
77 &name,
78 crate::observability::SpanAttributes {
79 route: Some("/:virtual"),
80 operation_id: Some(&operation_id),
81 ..Default::default()
82 },
83 self.run(&mut request, false, context, Some(route), handler),
84 ),
85 )
86 .await
87 }
88
89 pub async fn run<F, Fut>(
91 &self,
92 req: &mut AuthRequest,
93 http: bool,
94 context: &AuthContext<S>,
95 native_route: Option<AuthRoute>,
96 handler: F,
97 ) -> AuthResult<AuthResponse>
98 where
99 F: FnOnce(AuthRequest) -> Fut,
100 Fut: Future<Output = AuthResult<AuthResponse>>,
101 {
102 let base_path = context.base_path();
107 let stripped_path = if !http && !base_path.is_empty() && base_path != "/" {
108 req.path().strip_prefix(base_path).unwrap_or(req.path())
109 } else {
110 req.path()
111 };
112
113 let mut internal_req = if stripped_path != req.path() {
115 let mut r = req.clone();
116 r.path = stripped_path.to_string();
117 r
118 } else {
119 req.clone()
120 };
121 internal_req.set_endpoint_body(internal_req.unvalidated_input()?);
122 update_request_hook_context(&internal_req)?;
123
124 let mut input_patch = crate::endpoint_input::EndpointInputPatch::default();
125 if let Some(hook) = &self.hooks.before {
126 let action = match crate::observability::instrumentation::with_endpoint_hook(
127 &context.config,
128 &internal_req,
129 "before",
130 "user",
131 hook.before(&internal_req, context),
132 )
133 .await
134 {
135 Ok(action) => action,
136 Err(error) => return Err(endpoint_error(error, &internal_req)?),
137 };
138 if let Some(response) =
139 apply_before_action(action, &mut internal_req, req, &mut input_patch)?
140 {
141 return Ok(response);
142 }
143 }
144
145 for plugin in self.plugins.iter() {
148 let action = match plugin.before_request(&internal_req, context).await {
149 Ok(action) => action,
150 Err(error) => return Err(endpoint_error(error, &internal_req)?),
151 };
152 if let Some(response) =
153 apply_before_action(action, &mut internal_req, req, &mut input_patch)?
154 {
155 return Ok(response);
156 }
157 }
158
159 if http && internal_req.original_request().is_none() {
160 internal_req = internal_req.with_original_request(req.clone());
161 }
162 input_patch.apply(&mut internal_req)?;
163 update_request_hook_context(&internal_req)?;
164 let route = native_route.or_else(|| {
165 self.routes
166 .iter()
167 .find(|route| route.matches(internal_req.method(), internal_req.path()))
168 .cloned()
169 });
170 let snapshot = crate::hooks::current_request_hook_context();
171 let route_name = if internal_req.is_server_only() {
172 "/:virtual"
173 } else {
174 snapshot.as_ref().map_or(internal_req.path(), |snapshot| {
175 snapshot.path.as_deref().unwrap_or("/:virtual")
176 })
177 };
178 let operation_id = snapshot
179 .as_ref()
180 .and_then(|snapshot| snapshot.operation_id.as_deref())
181 .unwrap_or(route_name);
182 let result = crate::observability::with_span(
183 &context.config.experimental.instrumentation,
184 &format!("handler {route_name}"),
185 crate::observability::SpanAttributes {
186 route: Some(route_name),
187 operation_id: Some(operation_id),
188 ..Default::default()
189 },
190 async {
191 let input = async {
192 let body = match route
193 .as_ref()
194 .and_then(|route| route.body_validator.as_ref())
195 {
196 Some(validate) => validate.validate(&internal_req).await?,
197 None => internal_req.unvalidated_input()?,
198 };
199 let query = match route.as_ref().and_then(|route| route.query_validator) {
200 Some(validate) => validate(internal_req.query.clone())?,
201 None => internal_req.query.clone(),
202 };
203 if route.as_ref().is_some_and(|route| route.require_headers)
204 && internal_req.endpoint_headers().is_none()
205 {
206 return Err(AuthError::Upstream {
207 status: 400,
208 code: "VALIDATION_ERROR",
209 message: "Headers is required",
210 });
211 }
212 let mut request = internal_req.clone();
213 request.set_endpoint_body(body);
214 request.query = query;
215 Ok(request)
216 }
217 .await;
218 match input {
219 Ok(mut request) => {
220 if request.is_server_only() {
221 request.path = "virtual:".into();
222 }
223 let future = crate::endpoint_input::with_validated_input(
224 request.input_field_value()?,
225 request.query.clone(),
226 handler(request.clone()),
227 );
228 if request.is_server_only() {
229 let mut scope = match crate::hooks::current_request_hook_context() {
230 Some(scope) => scope,
231 None => RequestHookContext::from_request(&request)?,
232 };
233 scope.path = Some("virtual:".into());
234 scope.request = request.clone();
235 scope.query = request.query.clone();
236 scope.body = request.input_field_value()?;
237 scope.operation_id =
238 route.as_ref().map(|route| route.operation_id.clone());
239 with_request_hook_context_value(scope, future).await
240 } else {
241 future.await
242 }
243 }
244 Err(error) => Err(error),
245 }
246 },
247 )
248 .await;
249 let mut response = match result {
250 Ok(response) => response,
251 Err(error) if error.is_api_error() => error.to_auth_response(),
252 Err(error) => return Err(error),
253 };
254 context
255 .session_manager()
256 .finish_response(&internal_req, &mut response)?;
257 if let Some(hook) = &self.hooks.after {
258 let result = crate::observability::instrumentation::with_endpoint_hook(
259 &context.config,
260 &internal_req,
261 "after",
262 "user",
263 hook.after(&internal_req, &mut response, context),
264 )
265 .await;
266 apply_after_result(result, &internal_req, &mut response)?;
267 }
268 for plugin in self.plugins.iter() {
269 let result = plugin
270 .after_request(&internal_req, &mut response, context)
271 .await;
272 apply_after_result(result, &internal_req, &mut response)?;
273 }
274 if response.is_api_error() && !http {
275 response.capture_error_headers(response.headers.clone());
276 Err(response.into())
277 } else {
278 Ok(response)
279 }
280 }
281}
282fn endpoint_error(error: AuthError, request: &AuthRequest) -> AuthResult<AuthError> {
283 if !error.is_api_error() {
284 return Ok(error);
285 }
286 Ok(error.capture_endpoint_headers(request.take_response_headers()?))
287}
288
289fn apply_before_action(
290 action: Option<BeforeRequestAction>,
291 internal: &mut AuthRequest,
292 original: &mut AuthRequest,
293 input_patch: &mut crate::endpoint_input::EndpointInputPatch,
294) -> AuthResult<Option<AuthResponse>> {
295 match action {
296 Some(BeforeRequestAction::Respond(mut response)) => {
297 response.headers.merge(internal.take_response_headers()?);
298 return Ok(Some(response));
299 }
300 Some(BeforeRequestAction::MergeContext(patch)) => input_patch.merge(patch),
301 Some(BeforeRequestAction::ReplaceBody(body)) => {
302 internal.body = Some(body.clone());
303 original.body = Some(body);
304 let raw_body = internal.input_body()?;
305 internal.set_endpoint_body(crate::endpoint_input::ValidatedBody::unvalidated(raw_body));
306 update_request_hook_context(internal)?;
307 }
308 Some(BeforeRequestAction::InjectSession { session }) => {
309 internal.set_virtual_session(*session)
310 }
311 None => (),
312 }
313 Ok(None)
314}
315fn apply_after_result(
316 result: AuthResult<()>,
317 request: &AuthRequest,
318 response: &mut AuthResponse,
319) -> AuthResult<()> {
320 match result {
321 Ok(()) => response.headers.merge(request.take_response_headers()?),
322 Err(error) if error.is_api_error() => {
323 response.headers.merge(request.take_response_headers()?);
324 response.replace_returned(error.to_auth_response());
325 }
326 Err(error) => return Err(error),
327 }
328 Ok(())
329}
330
331impl<S: AuthSchema> AuthContext<S> {
332 pub async fn dispatch_native<F, Fut>(
334 &self,
335 source: crate::NativeRequest<'_>,
336 route: AuthRoute,
337 body: Option<serde_json::Value>,
338 query: Option<serde_json::Value>,
339 handler: F,
340 ) -> AuthResult<AuthResponse>
341 where
342 F: FnOnce(AuthRequest, Arc<AuthContext<S>>) -> Fut + Send,
343 Fut: Future<Output = AuthResult<AuthResponse>> + Send,
344 {
345 let mut request = native_request(source, &route);
346 request.body = body.as_ref().map(serde_json::to_vec).transpose()?;
347 request.set_endpoint_body(crate::endpoint_input::ValidatedBody::unvalidated(body));
348 request.query = query;
349 self.dispatch_native_request(source, route, request, handler)
350 .await
351 }
352
353 pub async fn dispatch_native_value<F, Fut>(
355 &self,
356 source: crate::NativeRequest<'_>,
357 route: AuthRoute,
358 body: crate::FieldValue,
359 query: Option<serde_json::Value>,
360 handler: F,
361 ) -> AuthResult<AuthResponse>
362 where
363 F: FnOnce(AuthRequest, Arc<AuthContext<S>>) -> Fut + Send,
364 Fut: Future<Output = AuthResult<AuthResponse>> + Send,
365 {
366 let mut request = native_request(source, &route);
367 request.set_endpoint_body(crate::endpoint_input::ValidatedBody::unvalidated_native(
368 body,
369 ));
370 request.query = query;
371 self.dispatch_native_request(source, route, request, handler)
372 .await
373 }
374
375 async fn dispatch_native_request<F, Fut>(
376 &self,
377 source: crate::NativeRequest<'_>,
378 route: AuthRoute,
379 request: AuthRequest,
380 handler: F,
381 ) -> AuthResult<AuthResponse>
382 where
383 F: FnOnce(AuthRequest, Arc<AuthContext<S>>) -> Fut + Send,
384 Fut: Future<Output = AuthResult<AuthResponse>> + Send,
385 {
386 self.with_native_context(source, |context| async move {
387 let dispatcher = context
388 .extensions
389 .get::<Arc<EndpointDispatcher<S>>>()
390 .ok_or_else(|| AuthError::config("Endpoint dispatcher is not initialized"))?;
391 dispatcher
392 .native(request, route, &context, |request| {
393 handler(request, context.clone())
394 })
395 .await
396 })
397 .await
398 }
399}
400
401fn native_request(source: crate::NativeRequest<'_>, route: &AuthRoute) -> AuthRequest {
402 let method = source
403 .request
404 .map_or_else(|| route.method.clone(), |request| request.method.clone());
405 let mut request = AuthRequest::new(method, "/").with_optional_headers(source.headers.cloned());
406 if let Some(original) = source.request {
407 request = request.with_original_request(original.clone());
408 }
409 request
410}