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