1use super::resolve_organization_id;
2use crate::organization::types::OrganizationResponse;
3use crate::organization::types::{
4 AcceptInvitationRequest, AcceptInvitationResponse, BasicMemberResponse,
5 CancelInvitationRequest, GetInvitationQuery, GetInvitationResponse, InviteMemberRequest,
6 ListInvitationsQuery, RejectInvitationRequest, UserInvitationResponse,
7};
8use crate::organization::{
9 InvitationLimit, OrganizationConfig, OrganizationInvitationContext,
10 OrganizationInvitationCreationContext, OrganizationInvitationDelivery,
11 OrganizationInvitationDraft, OrganizationInvitationLimitContext,
12};
13use alibi_core::entity::{AuthInvitation, AuthMember, AuthOrganization, AuthSession, AuthUser};
14use alibi_core::error::{AuthError, AuthResult};
15use alibi_core::plugin::AuthContext;
16use alibi_core::types::{AuthRequest, AuthResponse, CreateInvitation, InvitationStatus};
17use alibi_core::wire::InvitationView;
18use std::collections::HashMap;
19
20impl crate::organization::OrganizationPlugin {
21 pub async fn list_user_invitations(
31 &self,
32 ctx: &AuthContext<impl alibi_core::AuthSchema>,
33 email: &str,
34 ) -> AuthResult<Vec<UserInvitationResponse<InvitationView>>> {
35 list_user_invitations_for_email_core(email, ctx).await
36 }
37}
38
39pub(super) fn require_verified_invitation_email<S: alibi_core::AuthSchema>(
40 user: &impl AuthUser,
41 config: &OrganizationConfig,
42 ctx: &AuthContext<S>,
43 message: &'static str,
44) -> AuthResult<()> {
45 let required = config
46 .require_email_verification_on_invitation
47 .unwrap_or_else(|| ctx.config.advanced.database.serial_ids());
48 if required && !user.email_verified() {
49 return Err(AuthError::forbidden(message));
50 }
51 Ok(())
52}
53
54#[expect(
55 clippy::too_many_lines,
56 reason = "Keep invitation authorization, quota checks, and delivery callbacks in their required order"
57)]
58#[expect(
59 clippy::cast_precision_loss,
60 clippy::as_conversions,
61 reason = "Source compares stored counts as ECMAScript Numbers"
62)]
63pub(crate) async fn invite_member_core(
64 body: &InviteMemberRequest,
65 user: &impl AuthUser,
66 session: &impl AuthSession,
67 config: &OrganizationConfig,
68 ctx: &AuthContext<impl alibi_core::AuthSchema>,
69) -> AuthResult<InvitationView> {
70 let org_id =
71 resolve_organization_id(body.organization_id.as_deref(), None, session, ctx).await?;
72
73 if validator::Validate::validate(body).is_err() {
74 return Err(AuthError::Upstream {
75 status: 400,
76 code: "INVALID_EMAIL",
77 message: "Invalid email",
78 });
79 }
80 let member = ctx
81 .database
82 .get_member(&org_id, &user.id())
83 .await?
84 .ok_or_else(|| super::extension_common::org_error(400, "MEMBER_NOT_FOUND"))?;
85
86 if !super::extension_common::has_action(
87 member.role(),
88 "invitation",
89 "create",
90 config,
91 ctx,
92 &org_id,
93 )
94 .await?
95 {
96 return Err(super::extension_common::org_error(
97 403,
98 "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION",
99 ));
100 }
101
102 let roles = body.role.roles();
103
104 let mut valid_roles = vec!["owner".to_owned(), "admin".to_owned(), "member".to_owned()];
105 valid_roles.extend(
106 config
107 .roles
108 .iter()
109 .flat_map(|roles_2| roles_2.keys().cloned()),
110 );
111 if config.dynamic_access_control.enabled {
112 valid_roles.extend(
113 ctx.database
114 .list_organization_roles(&org_id)
115 .await?
116 .into_iter()
117 .map(|role| role.role),
118 );
119 }
120
121 let unknown_roles: Vec<_> = roles
122 .iter()
123 .copied()
124 .filter(|role| !valid_roles.iter().any(|valid| valid == role))
125 .collect();
126
127 if !unknown_roles.is_empty() {
128 return Err(AuthError::bad_request(format!(
132 "ROLE_NOT_FOUND: {}",
133 unknown_roles.join(", ")
134 )));
135 }
136
137 let member_is_creator = member
138 .role()
139 .split(',')
140 .map(str::trim)
141 .any(|role| role == config.effective_creator_role());
142 let invites_creator_role = roles
143 .iter()
144 .any(|role| *role == config.effective_creator_role());
145
146 if invites_creator_role && !member_is_creator {
147 return Err(AuthError::Upstream {
148 status: 403,
149 code: "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE",
150 message: "You are not allowed to invite a user with this role",
151 });
152 }
153
154 if let Some(existing_user) = ctx.database.get_user_by_email(&body.email).await?
155 && ctx
156 .database
157 .get_member(&org_id, &existing_user.id())
158 .await?
159 .is_some()
160 {
161 return Err(AuthError::bad_request(
162 "User is already a member of this organization",
163 ));
164 }
165
166 let email = body.email.to_lowercase();
167 let existing = ctx
168 .database
169 .pending_invitation_page(&org_id, Some(&email))
170 .await?
171 .into_iter()
172 .find(|invitation| !invitation.is_expired());
173 if existing.is_some()
174 && body.resend != Some(true)
175 && !config.cancel_pending_invitations_on_reinvite
176 {
177 return Err(super::extension_common::org_error(
178 400,
179 "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION",
180 ));
181 }
182 let organization = ctx
183 .database
184 .get_organization_by_id(&org_id)
185 .await?
186 .ok_or_else(|| super::extension_common::org_error(400, "ORGANIZATION_NOT_FOUND"))?;
187 let organization = OrganizationResponse::from_stored_organization(&organization)?;
188 let user_view = ctx.user_view(user);
189 let member_view = alibi_core::Member {
190 id: member.id().into_owned(),
191 organization_id: member.organization_id().into_owned(),
192 user_id: member.user_id().into_owned(),
193 role: member.role().to_owned(),
194 created_at: member.created_at(),
195 };
196 if let Some(existing) = existing {
197 if body.resend == Some(true) {
198 let updated = ctx
199 .database
200 .update_invitation_expiry(
201 &existing.id,
202 crate::organization::hooks::invitation::expiry(config.invitation_expires_in)?,
203 )
204 .await?;
205 let invitation = ctx.invitation_view(&updated);
206 crate::organization::hooks::invitation::deliver(
207 config,
208 OrganizationInvitationDelivery {
209 invitation: invitation.clone(),
210 organization,
211 inviter: member_view,
212 user: user_view,
213 },
214 ctx,
215 )
216 .await?;
217 return Ok(invitation);
218 }
219 _ = ctx
220 .database
221 .update_invitation_status(&existing.id, InvitationStatus::Canceled)
222 .await?;
223 }
224 let limit_context = OrganizationInvitationLimitContext {
225 user: user_view.clone(),
226 organization: organization.clone(),
227 member: member_view.clone(),
228 member_user: ctx.user_view(
229 &ctx.database
230 .get_user_by_id(&member.user_id())
231 .await?
232 .ok_or_else(|| super::extension_common::org_error(400, "MEMBER_NOT_FOUND"))?,
233 ),
234 };
235 let limit = match &config.invitation_limit {
236 Some(InvitationLimit::Fixed(value)) => *value,
237 Some(InvitationLimit::Resolver(resolver)) => {
238 resolver
239 .invitation_limit(&limit_context, &alibi_core::CallbackContext::new(ctx, None))
240 .await?
241 }
242 None => 100.0,
243 };
244 let pending = ctx.database.pending_invitation_page(&org_id, None).await?;
245 if pending
246 .iter()
247 .filter(|invitation| !invitation.is_expired())
248 .count() as f64
249 >= limit
250 {
251 return Err(super::extension_common::org_error(
252 403,
253 "INVITATION_LIMIT_REACHED",
254 ));
255 }
256 let requested_teams = {
257 body.team_id
258 .as_ref()
259 .map(|input| input.ids())
260 .unwrap_or_default()
261 };
262 let validate_teams = config.teams.enabled
263 && !matches!(&body.team_id, Some(crate::organization::types::TeamInput::One(id)) if id.is_empty());
264 if validate_teams && requested_teams.iter().any(|id| id.contains(',')) {
265 return Err(super::extension_common::org_error(400, "INVALID_TEAM_ID"));
266 }
267 for team_id in requested_teams.iter().filter(|_| validate_teams) {
268 _ = ctx
269 .database
270 .get_team(Some(&org_id), team_id)
271 .await?
272 .ok_or_else(|| super::extension_common::org_error(400, "TEAM_NOT_FOUND"))?;
273 }
274 for team_id in requested_teams.iter().filter(|_| {
275 validate_teams
276 && (config.teams.limit_resolver.is_some()
277 || config.teams.maximum_members_per_team.is_some())
278 }) {
279 let team = ctx
280 .database
281 .get_team(Some(&org_id), team_id)
282 .await?
283 .ok_or_else(|| super::extension_common::org_error(400, "TEAM_NOT_FOUND"))?;
284 let limit_context = crate::organization::extensions::TeamLimitContext {
285 organization_id: org_id.clone(),
286 team_id: Some(team.id.clone()),
287 session: Some(ctx.session_view(session)),
288 user: Some(ctx.user_view(user)),
289 request: alibi_core::hooks::current_request_hook_context()
290 .map(|context| context.request),
291 };
292 let maximum = match &config.teams.limit_resolver {
293 Some(resolver) => resolver.maximum_team_members(&limit_context).await?,
294 None => config.teams.maximum_members_per_team,
295 };
296 if let Some(limit) = maximum
297 && ctx.database.list_team_members(team_id).await?.len() as f64 >= limit
298 {
299 return Err(super::extension_common::org_error(
300 403,
301 "TEAM_MEMBER_LIMIT_REACHED",
302 ));
303 }
304 }
305
306 let mut draft = OrganizationInvitationDraft {
307 organization_id: org_id,
308 email,
309 role: body.role.joined(),
310 team_ids: requested_teams.iter().map(|id| (*id).to_owned()).collect(),
311 inviter_id: user.id().to_string(),
312 expires_at: None,
313 options: alibi_core::store::InvitationCreateOptions::default(),
314 };
315 if let Some(hooks) = &config.invitation_hooks
316 && let Some(patch) = hooks
317 .before_create_invitation(&OrganizationInvitationCreationContext {
318 invitation: draft.clone(),
319 inviter: user_view.clone(),
320 organization: organization.clone(),
321 })
322 .await?
323 {
324 patch.apply(&mut draft);
325 }
326 let invitation_data = CreateInvitation {
327 organization_id: draft.organization_id,
328 email: draft.email,
329 role: draft.role,
330 team_id: if draft.team_ids.is_empty() {
331 None
332 } else {
333 Some(draft.team_ids.join(","))
334 },
335 inviter_id: draft.inviter_id,
336 expires_at: match draft.expires_at {
337 Some(value) => value,
338 None => crate::organization::hooks::invitation::expiry(config.invitation_expires_in)?,
339 },
340 };
341 let invitation = ctx
342 .database
343 .create_invitation_with_options(invitation_data, draft.options)
344 .await?;
345 let invitation = ctx.invitation_view(&invitation);
346 crate::organization::hooks::invitation::deliver(
347 config,
348 OrganizationInvitationDelivery {
349 invitation: invitation.clone(),
350 organization: organization.clone(),
351 inviter: member_view,
352 user: user_view.clone(),
353 },
354 ctx,
355 )
356 .await?;
357 if let Some(hooks) = &config.invitation_hooks {
358 hooks
359 .after_create_invitation(&OrganizationInvitationContext {
360 invitation: invitation.clone(),
361 user: user_view,
362 organization,
363 })
364 .await?;
365 }
366 Ok(invitation)
367}
368
369pub(crate) async fn get_invitation_core(
370 query: &GetInvitationQuery,
371 user: &impl AuthUser,
372 config: &OrganizationConfig,
373 ctx: &AuthContext<impl alibi_core::AuthSchema>,
374) -> AuthResult<GetInvitationResponse<InvitationView>> {
375 let invitation = ctx
376 .database
377 .get_invitation_by_id(&query.id)
378 .await?
379 .filter(|invitation| invitation.is_pending() && !invitation.is_expired())
380 .ok_or_else(|| AuthError::bad_request("Invitation not found!"))?;
381 if user
382 .email()
383 .is_none_or(|email| email.to_lowercase() != invitation.email().to_lowercase())
384 {
385 return Err(AuthError::forbidden(
386 "You are not the recipient of the invitation",
387 ));
388 }
389 require_verified_invitation_email(
390 user,
391 config,
392 ctx,
393 "Email verification required to view or list invitations for the session email",
394 )?;
395
396 let organization = ctx
397 .database
398 .get_organization_by_id(&invitation.organization_id())
399 .await?
400 .ok_or_else(|| AuthError::bad_request("Organization not found"))?;
401 let inviter_member = ctx
402 .database
403 .get_member(&invitation.organization_id(), &invitation.inviter_id())
404 .await?
405 .ok_or_else(|| {
406 AuthError::bad_request("Inviter is no longer a member of the organization")
407 })?;
408 let inviter = ctx
409 .database
410 .get_user_by_id(&inviter_member.user_id())
411 .await?
412 .ok_or_else(|| {
413 AuthError::bad_request("Inviter is no longer a member of the organization")
414 })?;
415
416 Ok(GetInvitationResponse {
417 invitation: ctx.invitation_view(&invitation),
418 organization_name: organization.name().to_owned(),
419 organization_slug: organization.slug().to_owned(),
420 inviter_email: inviter.email().map(str::to_owned),
421 })
422}
423
424pub(crate) async fn list_invitations_core(
425 query: &ListInvitationsQuery,
426 user: &impl AuthUser,
427 session: &impl AuthSession,
428 ctx: &AuthContext<impl alibi_core::AuthSchema>,
429) -> AuthResult<Vec<InvitationView>> {
430 let org_id =
431 resolve_organization_id(query.organization_id.as_deref(), None, session, ctx).await?;
432
433 _ = ctx
434 .database
435 .get_member(&org_id, &user.id())
436 .await?
437 .ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;
438
439 let invitations = ctx.database.list_organization_invitations(&org_id).await?;
440 Ok(invitations
441 .iter()
442 .map(|invitation| ctx.invitation_view(invitation))
443 .collect())
444}
445
446pub(crate) async fn list_user_invitations_core(
447 user: &impl AuthUser,
448 ctx: &AuthContext<impl alibi_core::AuthSchema>,
449) -> AuthResult<Vec<UserInvitationResponse<InvitationView>>> {
450 if !user.email_verified() {
453 return Err(AuthError::forbidden(
454 "Email verification required to view or list invitations for the session email",
455 ));
456 }
457
458 list_user_invitations_for_email_core(user.email().unwrap_or_default(), ctx).await
459}
460
461pub(in crate::organization) async fn list_user_invitations_for_email_core(
462 email: &str,
463 ctx: &AuthContext<impl alibi_core::AuthSchema>,
464) -> AuthResult<Vec<UserInvitationResponse<InvitationView>>> {
465 if email.is_empty() {
466 return Err(AuthError::bad_request(
467 "Missing session headers, or email query parameter.",
468 ));
469 }
470 let invitations = ctx.database.list_user_invitations(email).await?;
472 let pending = invitations
473 .iter()
474 .filter(|invitation| invitation.status() == &InvitationStatus::Pending)
475 .collect::<Vec<_>>();
476 let organization_ids = pending
477 .iter()
478 .map(|invitation| invitation.organization_id().into_owned())
479 .collect::<Vec<_>>();
480 let organizations_by_id = ctx
481 .database
482 .list_organizations_by_ids(&organization_ids)
483 .await?
484 .into_iter()
485 .map(|organization| (organization.id.clone(), organization))
486 .collect::<HashMap<_, _>>();
487 Ok(pending
488 .into_iter()
489 .map(|invitation| UserInvitationResponse {
490 invitation: ctx.invitation_view(invitation),
491 organization_name: organizations_by_id
492 .get(invitation.organization_id().as_ref())
493 .map(|organization| organization.name().to_owned()),
494 })
495 .collect())
496}
497
498pub(crate) async fn reject_invitation_core(
499 body: &RejectInvitationRequest,
500 user: &impl AuthUser,
501 config: &OrganizationConfig,
502 ctx: &AuthContext<impl alibi_core::AuthSchema>,
503) -> AuthResult<AcceptInvitationResponse<InvitationView, Option<BasicMemberResponse>>> {
504 let invitation = ctx
505 .database
506 .get_invitation_by_id(&body.invitation_id)
507 .await?
508 .filter(alibi_core::Invitation::is_pending)
509 .ok_or(AuthError::Upstream {
510 status: 400,
511 code: "INVITATION_NOT_FOUND",
512 message: "Invitation not found!",
513 })?;
514
515 let user_email = user
516 .email()
517 .ok_or_else(|| AuthError::bad_request("User has no email"))?;
518
519 if invitation.email().to_lowercase() != user_email.to_lowercase() {
520 return Err(AuthError::forbidden(
521 "You are not the recipient of the invitation",
522 ));
523 }
524 require_verified_invitation_email(
525 user,
526 config,
527 ctx,
528 "Email verification required before accepting or rejecting invitation",
529 )?;
530
531 let organization = ctx
532 .database
533 .get_organization_by_id(&invitation.organization_id())
534 .await?
535 .ok_or_else(|| super::extension_common::org_error(400, "ORGANIZATION_NOT_FOUND"))?;
536 let mut hook_context = OrganizationInvitationContext {
537 invitation: ctx.invitation_view(&invitation),
538 user: ctx.user_view(user),
539 organization: OrganizationResponse::from_stored_organization(&organization)?,
540 };
541 if let Some(hooks) = &config.invitation_hooks {
542 hooks.before_reject_invitation(&hook_context).await?;
543 }
544 let updated_invitation = ctx
545 .database
546 .update_invitation_status(&invitation.id(), InvitationStatus::Rejected)
547 .await?;
548
549 hook_context.invitation = ctx.invitation_view(&updated_invitation);
550 if let Some(hooks) = &config.invitation_hooks {
551 hooks.after_reject_invitation(&hook_context).await?;
552 }
553 Ok(AcceptInvitationResponse {
554 invitation: hook_context.invitation,
555 member: None,
556 })
557}
558
559pub(crate) async fn cancel_invitation_core(
560 body: &CancelInvitationRequest,
561 user: &impl AuthUser,
562 config: &OrganizationConfig,
563 ctx: &AuthContext<impl alibi_core::AuthSchema>,
564) -> AuthResult<InvitationView> {
565 let invitation = ctx
566 .database
567 .get_invitation_by_id(&body.invitation_id)
568 .await?
569 .ok_or_else(|| super::extension_common::org_error(400, "INVITATION_NOT_FOUND"))?;
570
571 let member = ctx
572 .database
573 .get_member(invitation.organization_id().as_ref(), &user.id())
574 .await?
575 .ok_or_else(|| super::extension_common::org_error(400, "MEMBER_NOT_FOUND"))?;
576
577 if !super::extension_common::has_action(
578 member.role(),
579 "invitation",
580 "cancel",
581 config,
582 ctx,
583 invitation.organization_id().as_ref(),
584 )
585 .await?
586 {
587 return Err(AuthError::Upstream {
588 status: 403,
589 code: "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION",
590 message: "You are not allowed to cancel this invitation",
591 });
592 }
593
594 let organization = ctx
595 .database
596 .get_organization_by_id(&invitation.organization_id())
597 .await?
598 .ok_or_else(|| super::extension_common::org_error(400, "ORGANIZATION_NOT_FOUND"))?;
599 let mut hook_context = OrganizationInvitationContext {
600 invitation: ctx.invitation_view(&invitation),
601 user: ctx.user_view(user),
602 organization: OrganizationResponse::from_stored_organization(&organization)?,
603 };
604 if let Some(hooks) = &config.invitation_hooks {
605 hooks.before_cancel_invitation(&hook_context).await?;
606 }
607 let updated_invitation = ctx
608 .database
609 .update_invitation_status(&invitation.id(), InvitationStatus::Canceled)
610 .await?;
611
612 hook_context.invitation = ctx.invitation_view(&updated_invitation);
613 if let Some(hooks) = &config.invitation_hooks {
614 hooks.after_cancel_invitation(&hook_context).await?;
615 }
616 Ok(hook_context.invitation)
617}
618
619pub(crate) async fn handle_invite_member(
620 req: &AuthRequest,
621 ctx: &AuthContext<impl alibi_core::AuthSchema>,
622 config: &OrganizationConfig,
623) -> AuthResult<AuthResponse> {
624 let body = match super::org_input::invitation_create(req) {
625 Ok(body) => body,
626 Err(response) => return Ok(response),
627 };
628 let (user, session) = super::extension_common::session(req, ctx).await?;
629 let invitation = match invite_member_core(&body, &user, &session, config, ctx).await {
630 Err(AuthError::Internal(_) | AuthError::Database(_)) => return Ok(AuthResponse::new(500)),
631 result => result?,
632 };
633 Ok(AuthResponse::json(200, &invitation)?)
634}
635
636pub(crate) async fn handle_get_invitation(
637 req: &AuthRequest,
638 ctx: &AuthContext<impl alibi_core::AuthSchema>,
639 config: &OrganizationConfig,
640) -> AuthResult<AuthResponse> {
641 let (user, _) = match ctx.require_cached_session(req).await {
642 Ok(session) => session,
643 Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => {
644 return Ok(AuthResponse::json(
645 401,
646 &serde_json::json!({ "message": "Not authenticated" }),
647 )?);
648 }
649 Err(error) => return Err(error),
650 };
651 let query = parse_query::<GetInvitationQuery>(&req.query);
652 let response = match get_invitation_core(&query, &user, config, ctx).await {
653 Err(AuthError::BadRequest(message)) if message == "Invitation not found!" => {
656 return Ok(AuthResponse::json(
657 400,
658 &serde_json::json!({ "message": message }),
659 )?);
660 }
661 result => result?,
662 };
663 Ok(AuthResponse::json(200, &response)?)
664}
665
666pub(crate) async fn handle_list_invitations(
667 req: &AuthRequest,
668 ctx: &AuthContext<impl alibi_core::AuthSchema>,
669) -> AuthResult<AuthResponse> {
670 let (user, session) = super::extension_common::session(req, ctx).await?;
671 let query = parse_query::<ListInvitationsQuery>(&req.query);
672 let invitations = list_invitations_core(&query, &user, &session, ctx).await?;
673 Ok(AuthResponse::json(200, &invitations)?)
674}
675
676pub(crate) async fn handle_list_user_invitations(
677 req: &AuthRequest,
678 ctx: &AuthContext<impl alibi_core::AuthSchema>,
679) -> AuthResult<AuthResponse> {
680 if req
682 .query
683 .get("email")
684 .is_some_and(|email| !email.is_empty())
685 {
686 return Ok(AuthResponse::json(
687 400,
688 &serde_json::json!({
689 "message": "User email cannot be passed for client side API calls."
690 }),
691 )?);
692 }
693 let (user, _session) = match ctx.require_cached_session(req).await {
694 Ok(session) => session,
695 Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => {
696 return Ok(AuthResponse::json(
697 400,
698 &serde_json::json!({
699 "message": "Missing session headers, or email query parameter."
700 }),
701 )?);
702 }
703 Err(error) => return Err(error),
704 };
705 let invitations = list_user_invitations_core(&user, ctx).await?;
706 Ok(AuthResponse::json(200, &invitations)?)
707}
708
709pub(crate) async fn handle_accept_invitation(
710 req: &AuthRequest,
711 ctx: &AuthContext<impl alibi_core::AuthSchema>,
712 config: &OrganizationConfig,
713) -> AuthResult<AuthResponse> {
714 let (user, session) = super::extension_common::session(req, ctx).await?;
715 let body: AcceptInvitationRequest = match alibi_core::validate_request_body(req) {
716 Ok(value) => value,
717 Err(response) => return Ok(response),
718 };
719 let transport = super::invitation_acceptance::AcceptanceTransport::new(req);
720 let response =
721 match super::invitation_acceptance::accept(&body, &user, &session, config, ctx, &transport)
722 .await
723 {
724 Ok(response) => response,
725 Err(AuthError::Internal(_) | AuthError::Database(_)) => {
726 transport.discard_cookies();
729 return Ok(AuthResponse::new(500));
730 }
731 Err(error) => return Err(error),
732 };
733 Ok(AuthResponse::json(200, &response)?)
734}
735
736pub(crate) async fn handle_reject_invitation(
737 req: &AuthRequest,
738 ctx: &AuthContext<impl alibi_core::AuthSchema>,
739 config: &OrganizationConfig,
740) -> AuthResult<AuthResponse> {
741 let invitation_id = match super::org_input::invitation_id(req) {
742 Ok(id) => id,
743 Err(response) => return Ok(response),
744 };
745 let body = RejectInvitationRequest { invitation_id };
746 let (user, _session) = super::extension_common::session(req, ctx).await?;
747 let response = match reject_invitation_core(&body, &user, config, ctx).await {
748 Err(AuthError::Internal(_) | AuthError::Database(_)) => return Ok(AuthResponse::new(500)),
749 result => result?,
750 };
751 Ok(AuthResponse::json(200, &response)?)
752}
753
754pub(crate) async fn handle_cancel_invitation(
755 req: &AuthRequest,
756 ctx: &AuthContext<impl alibi_core::AuthSchema>,
757 config: &OrganizationConfig,
758) -> AuthResult<AuthResponse> {
759 let invitation_id = match super::org_input::invitation_id(req) {
760 Ok(id) => id,
761 Err(response) => return Ok(response),
762 };
763 let body = CancelInvitationRequest { invitation_id };
764 let (user, _session) = super::extension_common::session(req, ctx).await?;
765 let response = match cancel_invitation_core(&body, &user, config, ctx).await {
766 Err(AuthError::Internal(_) | AuthError::Database(_)) => return Ok(AuthResponse::new(500)),
767 result => result?,
768 };
769 Ok(AuthResponse::json(200, &response)?)
770}
771
772fn parse_query<T: Default + serde::de::DeserializeOwned>(query: &HashMap<String, String>) -> T {
773 let json_value = serde_json::to_value(query)
774 .unwrap_or(serde_json::Value::Object(serde_json::Map::default()));
775 serde_json::from_value(json_value).unwrap_or_default()
776}