Skip to main content

alibi_core/store/stateless/
invitations.rs

1use super::*;
2
3#[async_trait]
4impl InvitationStore for StatelessStore {
5    async fn create_invitation(&self, data: CreateInvitation) -> AuthResult<Invitation> {
6        self.create_invitation_with_options(data, InvitationCreateOptions::default())
7            .await
8    }
9    async fn create_invitation_with_options(
10        &self,
11        data: CreateInvitation,
12        options: InvitationCreateOptions,
13    ) -> AuthResult<Invitation> {
14        let row = Invitation {
15            id: options
16                .id
17                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
18            organization_id: data.organization_id,
19            email: data.email,
20            role: Some(data.role),
21            team_id: data.team_id,
22            status: options.status.unwrap_or_default(),
23            inviter_id: data.inviter_id,
24            expires_at: data.expires_at,
25            created_at: options.created_at.unwrap_or_else(Utc::now),
26        };
27        drop(
28            self.organization_state()?
29                .invitations
30                .insert(row.id.clone(), row.clone()),
31        );
32        Ok(row)
33    }
34    async fn get_invitation_by_id(&self, id: &str) -> AuthResult<Option<Invitation>> {
35        Ok(self.organization_state()?.invitations.get(id).cloned())
36    }
37    async fn get_pending_invitation(
38        &self,
39        org_id: &str,
40        email: &str,
41    ) -> AuthResult<Option<Invitation>> {
42        Ok(self
43            .organization_state()?
44            .invitations
45            .values()
46            .find(|row| {
47                row.organization_id == org_id
48                    && row.email == email.to_lowercase()
49                    && row.status == InvitationStatus::Pending
50                    && row.expires_at > Utc::now()
51            })
52            .cloned())
53    }
54    async fn pending_invitation_page(
55        &self,
56        org_id: &str,
57        email: Option<&str>,
58    ) -> AuthResult<Vec<Invitation>> {
59        let email = email.map(str::to_lowercase);
60        Ok(self
61            .organization_state()?
62            .invitations
63            .values()
64            .filter(|row| {
65                row.organization_id == org_id
66                    && row.status == InvitationStatus::Pending
67                    && email.as_ref().is_none_or(|email| row.email == *email)
68            })
69            .take(self.find_many_limit)
70            .cloned()
71            .collect())
72    }
73    async fn update_invitation_expiry(
74        &self,
75        id: &str,
76        expires_at: DateTime<Utc>,
77    ) -> AuthResult<Invitation> {
78        let mut state = self.organization_state()?;
79        let row = state
80            .invitations
81            .get_mut(id)
82            .ok_or_else(|| AuthError::not_found("Invitation not found"))?;
83        row.expires_at = expires_at;
84        Ok(row.clone())
85    }
86    async fn update_invitation_team_ids(
87        &self,
88        id: &str,
89        team_ids: Option<String>,
90    ) -> AuthResult<Invitation> {
91        let mut state = self.organization_state()?;
92        let row = state
93            .invitations
94            .get_mut(id)
95            .ok_or_else(|| AuthError::not_found("Invitation not found"))?;
96        row.team_id = team_ids;
97        Ok(row.clone())
98    }
99    async fn update_invitation_status(
100        &self,
101        id: &str,
102        status: InvitationStatus,
103    ) -> AuthResult<Invitation> {
104        let mut state = self.organization_state()?;
105        let row = state
106            .invitations
107            .get_mut(id)
108            .ok_or_else(|| AuthError::not_found("Invitation not found"))?;
109        row.status = status;
110        Ok(row.clone())
111    }
112    async fn update_invitation_status_if_status(
113        &self,
114        id: &str,
115        expected: InvitationStatus,
116        status: InvitationStatus,
117    ) -> AuthResult<Option<Invitation>> {
118        let mut state = self.organization_state()?;
119        let Some(row) = state
120            .invitations
121            .get_mut(id)
122            .filter(|row| row.status == expected)
123        else {
124            return Ok(None);
125        };
126        row.status = status;
127        Ok(Some(row.clone()))
128    }
129    async fn list_organization_invitations(&self, org_id: &str) -> AuthResult<Vec<Invitation>> {
130        Ok(self
131            .organization_state()?
132            .invitations
133            .values()
134            .filter(|row| row.organization_id == org_id)
135            .take(self.find_many_limit)
136            .cloned()
137            .collect())
138    }
139    async fn count_pending_organization_invitations(&self, org_id: &str) -> AuthResult<i64> {
140        i64::try_from(
141            self.organization_state()?
142                .invitations
143                .values()
144                .filter(|row| {
145                    row.organization_id == org_id
146                        && row.status == InvitationStatus::Pending
147                        && row.expires_at > Utc::now()
148                })
149                .count(),
150        )
151        .map_err(|_| AuthError::internal("Invitation count overflow"))
152    }
153    async fn list_user_invitations(&self, email: &str) -> AuthResult<Vec<Invitation>> {
154        let email = email.to_lowercase();
155        Ok(self
156            .organization_state()?
157            .invitations
158            .values()
159            .filter(|row| row.email == email)
160            .take(self.find_many_limit)
161            .cloned()
162            .collect())
163    }
164    // The legacy combined acceptance API requires a physical session and is
165    // intentionally unsupported. HTTP acceptance uses the status CAS and the
166    // transaction interface, whose initialized wrapper owns ephemeral sessions.
167}