alibi_api/plugins/organization/hooks/
invitation.rs1use crate::plugins::organization::types::OrganizationResponse;
2use alibi_core::AuthResult;
3use alibi_core::CallbackContext;
4use alibi_core::Member;
5use alibi_core::wire::InvitationView;
6use alibi_core::wire::UserView;
7use async_trait::async_trait;
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
11pub enum InvitationLimit {
12 Fixed(f64),
13 Resolver(Arc<dyn OrganizationInvitationLimitResolver>),
14}
15#[derive(Debug, Clone)]
16pub struct OrganizationInvitationLimitContext {
17 pub user: UserView,
18 pub organization: OrganizationResponse,
19 pub member: Member,
20 pub member_user: UserView,
22}
23#[async_trait]
24pub trait OrganizationInvitationLimitResolver: std::fmt::Debug + Send + Sync {
25 async fn invitation_limit(
26 &self,
27 context: &OrganizationInvitationLimitContext,
28 callback: &CallbackContext,
29 ) -> AuthResult<f64>;
30}
31#[derive(Debug, Clone)]
32pub struct OrganizationInvitationDraft {
33 pub email: String,
34 pub role: String,
35 pub organization_id: String,
36 pub inviter_id: String,
37 pub team_ids: Vec<String>,
38 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
39 pub options: alibi_core::store::InvitationCreateOptions,
40}
41#[derive(Debug, Clone, Default)]
42pub struct OrganizationInvitationCreatePatch {
43 pub email: Option<String>,
44 pub role: Option<String>,
45 pub organization_id: Option<String>,
46 pub inviter_id: Option<String>,
47 pub team_ids: Option<Vec<String>>,
48 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
49 pub id: Option<String>,
50 pub status: Option<alibi_core::InvitationStatus>,
51 pub created_at: Option<chrono::DateTime<chrono::Utc>>,
52}
53impl OrganizationInvitationCreatePatch {
54 pub(in crate::plugins::organization) fn apply(self, draft: &mut OrganizationInvitationDraft) {
55 if let Some(value) = self.email {
56 draft.email = value;
57 }
58 if let Some(value) = self.role {
59 draft.role = value;
60 }
61 if let Some(value) = self.organization_id {
62 draft.organization_id = value;
63 }
64 if let Some(value) = self.inviter_id {
65 draft.inviter_id = value;
66 }
67 if let Some(value) = self.expires_at {
68 draft.expires_at = Some(value);
69 }
70 if let Some(value) = self.id {
71 draft.options.id = Some(value);
72 }
73 if let Some(value) = self.status {
74 draft.options.status = Some(value);
75 }
76 if let Some(value) = self.created_at {
77 draft.options.created_at = Some(value);
78 }
79 if let Some(value) = self.team_ids {
80 draft.team_ids = value;
81 }
82 }
83}
84#[derive(Debug, Clone)]
85pub struct OrganizationInvitationCreationContext {
86 pub invitation: OrganizationInvitationDraft,
87 pub inviter: UserView,
88 pub organization: OrganizationResponse,
89}
90#[derive(Debug, Clone)]
91pub struct OrganizationInvitationContext {
92 pub invitation: InvitationView,
93 pub user: UserView,
95 pub organization: OrganizationResponse,
96}
97#[async_trait]
99pub trait OrganizationInvitationHooks: std::fmt::Debug + Send + Sync {
100 async fn before_create_invitation(
101 &self,
102 _context: &OrganizationInvitationCreationContext,
103 ) -> AuthResult<Option<OrganizationInvitationCreatePatch>> {
104 Ok(None)
105 }
106 async fn after_create_invitation(
107 &self,
108 _context: &OrganizationInvitationContext,
109 ) -> AuthResult<()> {
110 Ok(())
111 }
112 async fn before_reject_invitation(
113 &self,
114 _context: &OrganizationInvitationContext,
115 ) -> AuthResult<()> {
116 Ok(())
117 }
118 async fn after_reject_invitation(
119 &self,
120 _context: &OrganizationInvitationContext,
121 ) -> AuthResult<()> {
122 Ok(())
123 }
124 async fn before_cancel_invitation(
125 &self,
126 _context: &OrganizationInvitationContext,
127 ) -> AuthResult<()> {
128 Ok(())
129 }
130 async fn after_cancel_invitation(
131 &self,
132 _context: &OrganizationInvitationContext,
133 ) -> AuthResult<()> {
134 Ok(())
135 }
136}
137#[derive(Debug, Clone)]
138pub struct OrganizationInvitationDelivery {
139 pub invitation: InvitationView,
140 pub organization: OrganizationResponse,
141 pub inviter: Member,
142 pub user: UserView,
143}
144impl OrganizationInvitationDelivery {
145 #[must_use]
147 pub fn email(&self) -> String {
148 self.invitation.email.to_lowercase()
149 }
150}
151#[async_trait]
154pub trait OrganizationInvitationEmailSender: std::fmt::Debug + Send + Sync {
155 async fn send_invitation_email(
156 &self,
157 delivery: &OrganizationInvitationDelivery,
158 callback: &CallbackContext,
159 ) -> AuthResult<()>;
160}
161
162pub(in crate::plugins::organization) async fn deliver<S: alibi_core::AuthSchema>(
163 config: &crate::plugins::organization::OrganizationConfig,
164 delivery: OrganizationInvitationDelivery,
165 ctx: &alibi_core::AuthContext<S>,
166) -> AuthResult<()> {
167 let Some(sender) = config.send_invitation_email.clone() else {
168 return Ok(());
169 };
170 let callback = CallbackContext::new(ctx, None);
171 let work = async move {
172 if let Err(error) = sender.send_invitation_email(&delivery, &callback).await {
173 tracing::error!(%error, "Failed to run background task");
174 }
175 Ok(())
176 };
177 if let Some(handler) = &ctx.config.background_tasks {
178 let completion = alibi_core::start_background_task(work).await?;
179 if let Err(error) = handler.handle(completion) {
180 tracing::error!(%error, "Failed to run background task");
181 }
182 } else {
183 work.await?;
184 }
185 Ok(())
186}
187
188#[expect(
189 clippy::as_conversions,
190 clippy::cast_possible_truncation,
191 clippy::cast_precision_loss,
192 reason = "ECMAScript Date truncates the absolute millisecond timestamp"
193)]
194pub(in crate::plugins::organization) fn expiry(
195 seconds: Option<f64>,
196) -> AuthResult<chrono::DateTime<chrono::Utc>> {
197 let span = seconds
198 .filter(|value| crate::plugins::organization::policy::truthy_number(*value))
199 .unwrap_or(172_800.0);
200 let milliseconds = chrono::Utc::now().timestamp_millis() as f64 + span * 1000.0;
201 if !milliseconds.is_finite() || milliseconds.abs() > 8_640_000_000_000_000.0 {
203 return Err(alibi_core::AuthError::Config(
204 "Invitation expiry is outside the representable date range".into(),
205 ));
206 }
207 chrono::DateTime::from_timestamp_millis(milliseconds as i64).ok_or_else(|| {
208 alibi_core::AuthError::Config(
209 "Invitation expiry is outside the representable date range".into(),
210 )
211 })
212}
213
214#[derive(Debug, Clone)]
215pub struct OrganizationInvitationAcceptanceContext {
216 pub invitation: InvitationView,
217 pub user: UserView,
218 pub organization: OrganizationResponse,
220}
221
222#[derive(Debug, Clone)]
223pub struct OrganizationInvitationAcceptedContext {
224 pub invitation: InvitationView,
225 pub member: Member,
226 pub user: UserView,
227 pub organization: OrganizationResponse,
228}
229
230#[async_trait]
236pub trait OrganizationInvitationAcceptanceHooks: std::fmt::Debug + Send + Sync {
237 async fn before_accept_invitation(
238 &self,
239 _context: &OrganizationInvitationAcceptanceContext,
240 ) -> AuthResult<()> {
241 Ok(())
242 }
243 async fn after_accept_invitation(
244 &self,
245 _context: &OrganizationInvitationAcceptedContext,
246 ) -> AuthResult<()> {
247 Ok(())
248 }
249}