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