1use super::helpers::{
4 apply_default_role, completed_response_session, delete_session_cookie_headers, get_cookie,
5 issue_user_session_record, record_completed_session_record, response_has_session_cookie,
6};
7use alibi_core::error::codes;
8use alibi_core::wire::{SessionView, UserView};
9use alibi_core::{
10 AuthContext, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
11 AuthSchema, AuthSession, AuthUser, BeforeRequestAction, CreateUser, HttpMethod, RequestMeta,
12};
13use async_trait::async_trait;
14use rand::distr::{Alphanumeric, SampleString};
15use serde_json::json;
16use std::sync::Arc;
17
18#[async_trait]
20pub trait AnonymousIdentity: Send + Sync {
21 async fn email(&self) -> AuthResult<Option<String>> {
22 Ok(None)
23 }
24 async fn name(&self, _request: Option<&AuthRequest>) -> AuthResult<Option<String>> {
25 Ok(None)
26 }
27}
28
29pub struct AnonymousLink {
31 pub anonymous_user: UserView,
32 pub anonymous_session: SessionView,
33 pub new_user: UserView,
34 pub new_session: SessionView,
35}
36
37#[async_trait]
39pub trait LinkAnonymousAccount: Send + Sync {
40 async fn link(
41 &self,
42 accounts: &AnonymousLink,
43 context: &alibi_core::CallbackContext,
44 ) -> AuthResult<()>;
45}
46
47#[derive(Clone, Default)]
48pub struct AnonymousConfig {
49 pub email_domain_name: Option<String>,
50 pub identity: Option<Arc<dyn AnonymousIdentity>>,
51 pub on_link_account: Option<Arc<dyn LinkAnonymousAccount>>,
52 pub disable_delete_anonymous_user: bool,
53}
54
55#[derive(Clone, Default)]
56pub struct AnonymousPlugin {
57 config: AnonymousConfig,
58}
59
60#[derive(Debug, serde::Deserialize)]
62pub struct AnonymousSignIn {
63 pub token: String,
64 pub user: Box<UserView>,
65}
66
67#[derive(Debug, serde::Deserialize)]
69pub struct AnonymousDeleted {
70 pub success: bool,
71}
72
73impl AnonymousPlugin {
74 #[must_use]
76 pub const fn delete_anonymous_user_endpoint()
77 -> alibi_core::endpoint::ServerEndpoint<AnonymousDeleted> {
78 alibi_core::endpoint::ServerEndpoint::new("anonymous", "deleteAnonymousUser")
79 }
80
81 #[must_use]
83 pub const fn sign_in_anonymous_endpoint()
84 -> alibi_core::endpoint::ServerEndpoint<AnonymousSignIn> {
85 alibi_core::endpoint::ServerEndpoint::new("anonymous", "signInAnonymous")
86 }
87
88 #[must_use]
89 pub fn new() -> Self {
90 Self::default()
91 }
92 #[must_use]
93 pub const fn with_config(config: AnonymousConfig) -> Self {
94 Self { config }
95 }
96
97 async fn sign_in<S: AuthSchema>(
98 &self,
99 req: &AuthRequest,
100 ctx: &AuthContext<S>,
101 ) -> AuthResult<AuthResponse> {
102 self.sign_in_core(req, ctx).await
103 }
104
105 #[expect(
106 clippy::too_many_lines,
107 reason = "Keep anonymous session reuse, user creation, and session issuance in request order"
108 )]
109 async fn sign_in_core<S: AuthSchema>(
110 &self,
111 req: &impl alibi_core::session::SessionRequest,
112 ctx: &AuthContext<S>,
113 ) -> AuthResult<AuthResponse> {
114 use alibi_core::utils::cookie_utils::{
115 create_session_cookie_with_max_age, create_session_like_cookie, related_cookie_name,
116 sign_cookie_value, verify_cookie_value,
117 };
118
119 if let Some((current, _)) = anonymous_session(req, ctx).await
120 && current.is_anonymous() == Some(true)
121 {
122 return Ok(error(
123 400,
124 codes::ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
125 "Anonymous users cannot sign in again anonymously",
126 ));
127 }
128 let custom = match &self.config.identity {
129 Some(identity) => identity.email().await.map_err(callback_error)?,
130 None => None,
131 };
132 let email = if let Some(email) = custom.filter(|value| !value.is_empty()) {
133 if !super::authentication_helpers::is_valid_email(&email) {
134 return Ok(error(
135 400,
136 codes::INVALID_EMAIL_FORMAT,
137 "Email was not generated in a valid format",
138 ));
139 }
140 email
141 } else {
142 let id = Alphanumeric.sample_string(&mut rand::rng(), 32);
143 self.config
144 .email_domain_name
145 .as_ref()
146 .filter(|domain| !domain.is_empty())
147 .map_or_else(
148 || format!("{id}@anonymous.placeholder.invalid"),
149 |domain| format!("temp-{id}@{domain}"),
150 )
151 };
152 let name = match &self.config.identity {
153 Some(identity) => identity
154 .name(req.physical_request())
155 .await
156 .map_err(callback_error)?,
157 None => None,
158 }
159 .filter(|name| !name.is_empty())
160 .unwrap_or_else(|| "Anonymous".to_owned());
161 let mut create = CreateUser::new().with_email(email).with_name(name);
162 create.is_anonymous = Some(true);
163 create.email_verified = Some(false);
164 apply_default_role(ctx, &mut create);
165 let user = ctx
166 .database
167 .create_user_with_source_record(
168 create,
169 alibi_core::user_validation::UserValidationSource::creation("anonymous"),
170 )
171 .await
172 .map_err(|error| match error {
173 alibi_core::AuthError::UserCreationCancelled => alibi_core::AuthError::Upstream {
174 status: 500,
175 code: codes::FAILED_TO_CREATE_USER,
176 message: "Failed to create user",
177 },
178 error @ (alibi_core::AuthError::Api { .. }
179 | alibi_core::AuthError::Upstream { .. }
180 | alibi_core::AuthError::BadRequest(_)
181 | alibi_core::AuthError::InvalidRequest(_)
182 | alibi_core::AuthError::Validation(_)
183 | alibi_core::AuthError::InvalidCredentials
184 | alibi_core::AuthError::Unauthenticated
185 | alibi_core::AuthError::AuthenticationFailed(_)
186 | alibi_core::AuthError::SessionNotFound
187 | alibi_core::AuthError::Forbidden(_)
188 | alibi_core::AuthError::SessionCreationCancelled
189 | alibi_core::AuthError::BannedUser(_)
190 | alibi_core::AuthError::Unauthorized
191 | alibi_core::AuthError::UserNotFound
192 | alibi_core::AuthError::NotFound(_)
193 | alibi_core::AuthError::Conflict(_)
194 | alibi_core::AuthError::MethodNotAllowed(_)
195 | alibi_core::AuthError::PayloadTooLarge(_)
196 | alibi_core::AuthError::UnprocessableEntity(_)
197 | alibi_core::AuthError::RateLimited { .. }
198 | alibi_core::AuthError::NotImplemented(_)
199 | alibi_core::AuthError::Config(_)
200 | alibi_core::AuthError::Database(_)
201 | alibi_core::AuthError::Serialization(_)
202 | alibi_core::AuthError::Plugin { .. }
203 | alibi_core::AuthError::CallbackFailure(_)
204 | alibi_core::AuthError::Internal(_)
205 | alibi_core::AuthError::Encryption(_)
206 | alibi_core::AuthError::PasswordHash(_)
207 | alibi_core::AuthError::Jwt(_)) => error,
208 })?;
209 let headers = req.session_headers();
210 let meta = if let Some(request) = req.physical_request() {
211 RequestMeta::from_request(request)
212 } else {
213 RequestMeta {
214 ip_address: Some(
215 ctx.config
216 .advanced
217 .ip_address
218 .resolve_ip(headers)
219 .unwrap_or_default(),
220 ),
221 user_agent: Some(headers.get("user-agent").cloned().unwrap_or_default()),
222 }
223 };
224 let issued =
225 issue_user_session_record(ctx, user.id().as_ref(), meta.ip_address, meta.user_agent)
226 .await
227 .map_err(|cause| match cause.into_auth_error() {
228 alibi_core::AuthError::SessionCreationCancelled => {
229 alibi_core::AuthError::Upstream {
230 status: 400,
231 code: codes::COULD_NOT_CREATE_SESSION,
232 message: "Could not create session",
233 }
234 }
235 cause @ (alibi_core::AuthError::Api { .. }
236 | alibi_core::AuthError::Upstream { .. }
237 | alibi_core::AuthError::BadRequest(_)
238 | alibi_core::AuthError::InvalidRequest(_)
239 | alibi_core::AuthError::Validation(_)
240 | alibi_core::AuthError::InvalidCredentials
241 | alibi_core::AuthError::Unauthenticated
242 | alibi_core::AuthError::AuthenticationFailed(_)
243 | alibi_core::AuthError::SessionNotFound
244 | alibi_core::AuthError::Forbidden(_)
245 | alibi_core::AuthError::UserCreationCancelled
246 | alibi_core::AuthError::BannedUser(_)
247 | alibi_core::AuthError::Unauthorized
248 | alibi_core::AuthError::UserNotFound
249 | alibi_core::AuthError::NotFound(_)
250 | alibi_core::AuthError::Conflict(_)
251 | alibi_core::AuthError::MethodNotAllowed(_)
252 | alibi_core::AuthError::PayloadTooLarge(_)
253 | alibi_core::AuthError::UnprocessableEntity(_)
254 | alibi_core::AuthError::RateLimited { .. }
255 | alibi_core::AuthError::NotImplemented(_)
256 | alibi_core::AuthError::Config(_)
257 | alibi_core::AuthError::Database(_)
258 | alibi_core::AuthError::Serialization(_)
259 | alibi_core::AuthError::Plugin { .. }
260 | alibi_core::AuthError::CallbackFailure(_)
261 | alibi_core::AuthError::Internal(_)
262 | alibi_core::AuthError::Encryption(_)
263 | alibi_core::AuthError::PasswordHash(_)
264 | alibi_core::AuthError::Jwt(_)) => cause,
265 })?;
266 record_completed_session_record::<S>(&user, &issued.session);
269 let mut response = AuthResponse::json(
270 200,
271 &json!({"token":issued.session.token(),"user":ctx.user_view(&user)}),
272 )?;
273
274 let preference_name = related_cookie_name(&ctx.config, "dont_remember");
275 let dont_remember = get_cookie(req, &preference_name)
276 .and_then(|value| verify_cookie_value(&value, ctx.config.current_secret()))
277 .is_some_and(|value| !value.is_empty());
278 response.headers.append(
279 "set-cookie",
280 create_session_cookie_with_max_age(
281 Some(issued.session.token()),
282 if dont_remember {
283 None
284 } else {
285 Some(ctx.config.session.expires_in.num_seconds())
286 },
287 &ctx.config,
288 )?,
289 );
290 if dont_remember {
291 response.headers.append(
292 "set-cookie",
293 create_session_like_cookie(
294 &preference_name,
295 &sign_cookie_value("true", ctx.config.current_secret()),
296 None,
297 &ctx.config,
298 )?,
299 );
300 }
301 Ok(response)
302 }
303
304 async fn delete(
305 &self,
306 req: &AuthRequest,
307 ctx: &AuthContext<impl AuthSchema>,
308 ) -> AuthResult<AuthResponse> {
309 self.delete_core(req, ctx).await
310 }
311
312 async fn delete_core(
313 &self,
314 req: &impl alibi_core::session::SessionRequest,
315 ctx: &AuthContext<impl AuthSchema>,
316 ) -> AuthResult<AuthResponse> {
317 let (user, _) =
318 ctx.require_authoritative_session(req)
319 .await
320 .map_err(|error| match error {
321 alibi_core::AuthError::Unauthenticated => alibi_core::AuthError::Upstream {
322 status: 401,
323 code: codes::UNAUTHORIZED,
324 message: "Unauthorized",
325 },
326 error @ (alibi_core::AuthError::Api { .. }
327 | alibi_core::AuthError::Upstream { .. }
328 | alibi_core::AuthError::BadRequest(_)
329 | alibi_core::AuthError::InvalidRequest(_)
330 | alibi_core::AuthError::Validation(_)
331 | alibi_core::AuthError::InvalidCredentials
332 | alibi_core::AuthError::AuthenticationFailed(_)
333 | alibi_core::AuthError::SessionNotFound
334 | alibi_core::AuthError::Forbidden(_)
335 | alibi_core::AuthError::SessionCreationCancelled
336 | alibi_core::AuthError::UserCreationCancelled
337 | alibi_core::AuthError::BannedUser(_)
338 | alibi_core::AuthError::Unauthorized
339 | alibi_core::AuthError::UserNotFound
340 | alibi_core::AuthError::NotFound(_)
341 | alibi_core::AuthError::Conflict(_)
342 | alibi_core::AuthError::MethodNotAllowed(_)
343 | alibi_core::AuthError::PayloadTooLarge(_)
344 | alibi_core::AuthError::UnprocessableEntity(_)
345 | alibi_core::AuthError::RateLimited { .. }
346 | alibi_core::AuthError::NotImplemented(_)
347 | alibi_core::AuthError::Config(_)
348 | alibi_core::AuthError::Database(_)
349 | alibi_core::AuthError::Serialization(_)
350 | alibi_core::AuthError::Plugin { .. }
351 | alibi_core::AuthError::CallbackFailure(_)
352 | alibi_core::AuthError::Internal(_)
353 | alibi_core::AuthError::Encryption(_)
354 | alibi_core::AuthError::PasswordHash(_)
355 | alibi_core::AuthError::Jwt(_)) => error,
356 })?;
357 if self.config.disable_delete_anonymous_user {
358 return Ok(error(
359 400,
360 codes::DELETE_ANONYMOUS_USER_DISABLED,
361 "Deleting anonymous users is disabled",
362 ));
363 }
364 if user.is_anonymous() != Some(true) {
365 return Ok(error(
366 403,
367 codes::USER_IS_NOT_ANONYMOUS,
368 "User is not anonymous",
369 ));
370 }
371 if let Err(cause) = ctx.database.delete_user_sessions(user.id().as_ref()).await {
372 tracing::error!(%cause, "Failed to delete anonymous user sessions");
373 return Ok(error(
374 500,
375 codes::FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS,
376 "Failed to delete anonymous user sessions",
377 ));
378 }
379 if let Err(cause) = ctx.database.delete_user(user.id().as_ref()).await {
380 tracing::error!(%cause, "Failed to delete anonymous user");
381 return Ok(error(
382 500,
383 codes::FAILED_TO_DELETE_ANONYMOUS_USER,
384 "Failed to delete anonymous user",
385 ));
386 }
387 let mut response = AuthResponse::json(200, &json!({"success":true}))?;
388 for cookie in delete_session_cookie_headers(&ctx.config)? {
389 response.headers.append("set-cookie", cookie);
390 }
391 Ok(response)
392 }
393}
394
395#[async_trait]
396impl<S: AuthSchema> AuthPlugin<S> for AnonymousPlugin {
397 route_openapi_metadata!(S);
398
399 fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> {
400 vec![self]
401 }
402
403 fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> {
404 vec![
405 crate::endpoint::definition(
406 "signInAnonymous",
407 "signInAnonymous",
408 Some("/sign-in/anonymous"),
409 HttpMethod::Post,
410 ),
411 crate::endpoint::definition(
412 "deleteAnonymousUser",
413 "deleteAnonymousUser",
414 Some("/delete-anonymous-user"),
415 HttpMethod::Post,
416 ),
417 ]
418 }
419 async fn on_endpoint(
420 &self,
421 call: &alibi_core::endpoint::EndpointCall,
422 ctx: &AuthContext<S>,
423 ) -> AuthResult<alibi_core::endpoint::EndpointResponse> {
424 let output = if call.operation_id() == "deleteAnonymousUser" {
425 self.delete_core(call, ctx).await?
426 } else {
427 self.sign_in_core(call, ctx).await?
428 };
429 let mut response = if output.status >= 400 {
430 alibi_core::endpoint::EndpointResponse::error(crate::endpoint::error_response(
431 output.clone(),
432 ))
433 } else {
434 alibi_core::endpoint::EndpointResponse::value(alibi_core::utils::json::from_slice(
435 &output.body,
436 )?)
437 };
438 response.set_status(Some(output.status));
439 response.merge_headers(output.headers);
440 Ok(response)
441 }
442 fn name(&self) -> &'static str {
443 "anonymous"
444 }
445 fn routes(&self) -> Vec<AuthRoute> {
446 vec![
447 AuthRoute::post("/sign-in/anonymous", "sign_in_anonymous"),
448 AuthRoute::post("/delete-anonymous-user", "delete_anonymous_user"),
449 ]
450 }
451 async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
452 ctx.set_metadata("anonymous.enabled", json!(true));
453 let default = |mut input: CreateUser| {
454 if input.is_anonymous.is_none() {
455 input.is_anonymous = Some(false);
456 }
457 Ok(input)
458 };
459 if ctx.config.user_validation.is_some() {
460 ctx.register_user_creation_adapter_default(default);
461 } else {
462 ctx.register_user_create_transform(default);
463 }
464 Ok(())
465 }
466 async fn on_request(
467 &self,
468 req: &AuthRequest,
469 ctx: &AuthContext<S>,
470 ) -> AuthResult<Option<AuthResponse>> {
471 match (req.method(), req.path()) {
472 (HttpMethod::Post, "/sign-in/anonymous") => Ok(Some(self.sign_in(req, ctx).await?)),
473 (HttpMethod::Post, "/delete-anonymous-user") => Ok(Some(self.delete(req, ctx).await?)),
474 _ => Ok(None),
475 }
476 }
477 async fn before_request(
478 &self,
479 req: &AuthRequest,
480 ctx: &AuthContext<S>,
481 ) -> AuthResult<Option<BeforeRequestAction>> {
482 if req.path() == "/sign-in/social"
483 && let Some((user, _)) = anonymous_session(req, ctx).await
484 && user.is_anonymous() == Some(true)
485 {
486 req.extensions()
487 .insert(super::oauth::CapturedOAuthServerContext(
488 super::oauth::OAuthServerContext {
489 anonymous_user_id: user.id().into_owned(),
490 },
491 ));
492 }
493 Ok(None)
494 }
495 async fn after_request(
496 &self,
497 req: &AuthRequest,
498 ctx: &AuthContext<S>,
499 response: AuthResponse,
500 ) -> AuthResult<AuthResponse> {
501 self.link_completed(req, req.path(), ctx, &response).await?;
502 Ok(response)
503 }
504}
505
506impl AnonymousPlugin {
507 async fn link_completed<S: AuthSchema>(
508 &self,
509 req: &impl alibi_core::session::SessionRequest,
510 path: &str,
511 ctx: &AuthContext<S>,
512 response: &AuthResponse,
513 ) -> AuthResult<()> {
514 let matches = [
515 "/sign-in",
516 "/sign-up",
517 "/callback",
518 "/magic-link/verify",
519 "/email-otp/verify-email",
520 "/one-tap/callback",
521 "/passkey/verify-authentication",
522 "/phone-number/verify",
523 "/verify-email",
524 ]
525 .iter()
526 .any(|prefix| path.starts_with(prefix));
527 if !matches {
528 return Ok(());
529 }
530 if !response_has_session_cookie(ctx, response) {
531 return Ok(());
532 }
533 let Some((old_user, old_session)) = resolve_anonymous_session(req, ctx).await? else {
534 return Ok(());
535 };
536 if old_user.is_anonymous != Some(true) {
537 return Ok(());
538 }
539 let Some(issued) = completed_response_session(req, ctx, response) else {
540 return Ok(());
541 };
542 if let Some(linker) = &self.config.on_link_account {
543 let mut new_session = issued.callback_session(ctx);
546 new_session.omitted_fields.clear();
547 linker
548 .link(
549 &AnonymousLink {
550 anonymous_user: old_user.clone(),
551 anonymous_session: ctx.session_view(&old_session),
552 new_user: issued.callback_user(ctx),
553 new_session,
554 },
555 &alibi_core::CallbackContext::new(ctx, req.physical_request()),
556 )
557 .await
558 .map_err(callback_error)?;
559 }
560 if self.config.disable_delete_anonymous_user
561 || old_user.id == issued.user.id().as_ref()
562 || issued.user.is_anonymous() == Some(true)
563 {
564 return Ok(());
565 }
566 if let Err(error) = ctx.database.delete_user(&old_user.id).await {
567 tracing::error!(user_id=%old_user.id,error=%error,"Failed to clean up anonymous account");
568 }
569 Ok(())
570 }
571}
572
573#[async_trait]
574impl<S: AuthSchema> alibi_core::endpoint::EndpointHook<S> for AnonymousPlugin {
575 fn matches_before(
576 &self,
577 call: &alibi_core::endpoint::EndpointCall,
578 _: &AuthContext<S>,
579 ) -> AuthResult<bool> {
580 Ok(call.path() == Some("/sign-in/social"))
581 }
582 async fn before(
583 &self,
584 call: &alibi_core::endpoint::EndpointCall,
585 ctx: &AuthContext<S>,
586 ) -> AuthResult<Option<alibi_core::endpoint::BeforeEndpointAction>> {
587 if let Some((user, _)) = anonymous_session(call, ctx).await
588 && user.is_anonymous() == Some(true)
589 {
590 call.extensions()
591 .insert(super::oauth::CapturedOAuthServerContext(
592 super::oauth::OAuthServerContext {
593 anonymous_user_id: user.id().into_owned(),
594 },
595 ));
596 }
597 Ok(None)
598 }
599 async fn after(
600 &self,
601 call: &alibi_core::endpoint::EndpointCall,
602 ctx: &AuthContext<S>,
603 response: alibi_core::endpoint::EndpointResponse,
604 ) -> AuthResult<alibi_core::endpoint::EndpointResponse> {
605 self.link_completed(
606 call,
607 call.path().unwrap_or_default(),
608 ctx,
609 &response.to_response()?,
610 )
611 .await?;
612 Ok(response)
613 }
614}
615
616impl std::fmt::Debug for AnonymousConfig {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 f.debug_struct("AnonymousConfig").finish_non_exhaustive()
619 }
620}
621
622impl std::fmt::Debug for AnonymousLink {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 f.debug_struct("AnonymousLink").finish_non_exhaustive()
625 }
626}
627
628impl std::fmt::Debug for AnonymousPlugin {
629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630 f.debug_struct("AnonymousPlugin").finish_non_exhaustive()
631 }
632}
633
634async fn anonymous_session<S: AuthSchema>(
637 req: &impl alibi_core::session::SessionRequest,
638 ctx: &AuthContext<S>,
639) -> Option<(alibi_core::AuthenticatedUser<S>, SessionView)> {
640 if let Some(call) = req.endpoint_call() {
641 let mut read = call.clone();
642 let mut query = call
643 .query()
644 .and_then(alibi_core::utils::json::JsValue::as_object)
645 .cloned()
646 .unwrap_or_default();
647 _ = query.insert(
648 "disableRefresh".into(),
649 alibi_core::utils::json::JsValue::Bool(true),
650 );
651 alibi_core::endpoint::EndpointContextPatch {
652 query: Some(alibi_core::utils::json::JsValue::Object(query)),
653 ..Default::default()
654 }
655 .apply(&mut read);
656 return ctx.require_cached_session(&read).await.ok();
657 }
658 let mut read = req.physical_request()?.clone();
659 _ = read.query.insert("disableRefresh".into(), "true".into());
660 ctx.require_cached_session(&read).await.ok()
661}
662
663async fn resolve_anonymous_session<S: AuthSchema>(
664 req: &impl alibi_core::session::SessionRequest,
665 ctx: &AuthContext<S>,
666) -> AuthResult<Option<(UserView, SessionView)>> {
667 if let Some((user, session)) = anonymous_session(req, ctx).await
668 && user.is_anonymous() == Some(true)
669 {
670 let user = match user {
671 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(&user),
672 alibi_core::AuthenticatedUser::Cached(user) => *user,
673 };
674 return Ok(Some((user, session)));
675 }
676 let Some(context) = req
677 .extensions()
678 .get::<super::oauth::RecoveredOAuthServerContext>()
679 else {
680 return Ok(None);
681 };
682 let Some(user) = ctx
683 .database
684 .get_user_by_id_record(&context.0.anonymous_user_id)
685 .await?
686 else {
687 return Ok(None);
688 };
689 if user.is_anonymous() != Some(true) {
690 return Ok(None);
691 }
692 let session = ctx
693 .database
694 .get_user_sessions(&context.0.anonymous_user_id)
695 .await?
696 .into_iter()
697 .find(|session| session.expires_at() > chrono::Utc::now());
698 Ok(session.map(|session| (ctx.trusted_user_view(&user), ctx.session_view(&session))))
699}
700
701fn callback_error(error: alibi_core::AuthError) -> alibi_core::AuthError {
704 match error {
705 alibi_core::AuthError::Internal(_) => {
706 alibi_core::AuthError::CallbackFailure(Box::new(error))
707 }
708 other => other,
709 }
710}
711
712fn error(status: u16, code: &'static str, message: &'static str) -> AuthResponse {
713 alibi_core::AuthError::Upstream {
714 status,
715 code,
716 message,
717 }
718 .to_auth_response()
719}