Skip to main content

alibi_core/store/stateless/
invitations.rs

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