Skip to main content

alibi_core/store/stateless/
teams.rs

1use crate::store::stateless::{OrganizationState, StatelessStore};
2use crate::store::{TeamStore, team_membership_key};
3use crate::types::AddTeamMemberResult;
4use crate::utils::javascript::number_from_i64;
5use crate::{AuthError, AuthResult, CreateTeam, InvitationStatus, Team, TeamMember, UpdateTeam};
6use async_trait::async_trait;
7use chrono::Utc;
8
9impl OrganizationState {
10    pub(super) fn remove_team_members(&mut self, team_id: &str, user_id: &str) -> usize {
11        let before = self.team_members.len();
12        self.team_members
13            .retain(|_, row| row.team_id != team_id || row.user_id != user_id);
14        let removed = before - self.team_members.len();
15        if let (Some(team), Ok(removed)) = (self.teams.get_mut(team_id), i64::try_from(removed))
16            && team.member_count >= removed
17        {
18            team.member_count -= removed;
19        }
20        removed
21    }
22}
23#[async_trait]
24impl TeamStore for StatelessStore {
25    async fn create_team(&self, data: CreateTeam) -> AuthResult<Team> {
26        let row = Team {
27            id: uuid::Uuid::new_v4().to_string(),
28            name: data.name,
29            organization_id: data.organization_id,
30            created_at: data.updated_at.unwrap_or_else(Utc::now),
31            updated_at: data.updated_at,
32            member_count: 0,
33        };
34        _ = self
35            .organization_state()?
36            .teams
37            .insert(row.id.clone(), row.clone());
38        Ok(row)
39    }
40    async fn get_team(&self, organization_id: Option<&str>, id: &str) -> AuthResult<Option<Team>> {
41        Ok(self
42            .organization_state()?
43            .teams
44            .get(id)
45            .filter(|row| organization_id.is_none_or(|org| org == row.organization_id))
46            .cloned())
47    }
48    async fn list_teams(&self, organization_id: &str) -> AuthResult<Vec<Team>> {
49        Ok(self
50            .organization_state()?
51            .teams
52            .values()
53            .filter(|row| row.organization_id == organization_id)
54            .take(self.find_many_limit)
55            .cloned()
56            .collect())
57    }
58    async fn update_team(
59        &self,
60        organization_id: &str,
61        id: &str,
62        update: UpdateTeam,
63    ) -> AuthResult<Team> {
64        let mut state = self.organization_state()?;
65        let row = state
66            .teams
67            .get_mut(id)
68            .filter(|row| row.organization_id == organization_id)
69            .ok_or_else(|| AuthError::bad_request("Team not found"))?;
70        if let Some(name) = update.name {
71            row.name = name;
72        }
73        row.updated_at = Some(Utc::now());
74        Ok(row.clone())
75    }
76    async fn delete_team(&self, organization_id: &str, id: &str) -> AuthResult<bool> {
77        let mut state = self.organization_state()?;
78        if state
79            .teams
80            .get(id)
81            .is_none_or(|row| row.organization_id != organization_id)
82        {
83            return Ok(false);
84        }
85        _ = state.teams.shift_remove(id);
86        state.team_members.retain(|_, row| row.team_id != id);
87        for invite in state
88            .invitations
89            .values_mut()
90            .filter(|row| {
91                row.organization_id == organization_id && row.status == InvitationStatus::Pending
92            })
93            .take(self.find_many_limit)
94        {
95            if invite.expires_at <= Utc::now() {
96                continue;
97            }
98            if let Some(ids) = invite
99                .team_id
100                .as_deref()
101                .filter(|ids| ids.split(',').any(|part| part == id))
102            {
103                let remaining = ids
104                    .split(',')
105                    .filter(|part| *part != id)
106                    .collect::<Vec<_>>()
107                    .join(",");
108                invite.team_id = (!remaining.is_empty()).then_some(remaining);
109            }
110        }
111        Ok(true)
112    }
113    async fn get_team_member(
114        &self,
115        team_id: &str,
116        user_id: &str,
117    ) -> AuthResult<Option<TeamMember>> {
118        Ok(self
119            .organization_state()?
120            .team_members
121            .values()
122            .find(|row| row.team_id == team_id && row.user_id == user_id)
123            .cloned())
124    }
125    async fn add_team_member(
126        &self,
127        team_id: &str,
128        user_id: &str,
129        maximum: Option<f64>,
130    ) -> AuthResult<AddTeamMemberResult> {
131        let key = team_membership_key(team_id, user_id)?;
132        let mut state = self.organization_state()?;
133        if let Some(row) = state
134            .team_members
135            .values()
136            .find(|row| row.team_id == team_id && row.user_id == user_id)
137        {
138            return Ok(AddTeamMemberResult::Existing(row.clone()));
139        }
140        let count = i64::try_from(
141            state
142                .team_members
143                .values()
144                .filter(|row| row.team_id == team_id)
145                .count(),
146        )
147        .map_err(|_| AuthError::internal("Team membership count overflow"))?;
148        let team = state
149            .teams
150            .get_mut(team_id)
151            .ok_or_else(|| AuthError::bad_request("Team not found"))?;
152        team.member_count = team.member_count.max(count);
153        // Keep the raw Number predicate, including fractions, NaN and infinities.
154        if maximum.is_some_and(|maximum| {
155            number_from_i64(team.member_count).partial_cmp(&maximum)
156                != Some(std::cmp::Ordering::Less)
157        }) {
158            return Ok(AddTeamMemberResult::LimitReached);
159        }
160        team.member_count = team
161            .member_count
162            .checked_add(1)
163            .ok_or_else(|| AuthError::internal("Team membership count overflow"))?;
164        let row = TeamMember {
165            id: uuid::Uuid::new_v4().to_string(),
166            team_id: team_id.into(),
167            user_id: user_id.into(),
168            created_at: Some(Utc::now()),
169            membership_key: Some(key),
170        };
171        _ = state.team_members.insert(row.id.clone(), row.clone());
172        Ok(AddTeamMemberResult::Added(row))
173    }
174    async fn remove_team_member(&self, team_id: &str, user_id: &str) -> AuthResult<usize> {
175        Ok(self
176            .organization_state()?
177            .remove_team_members(team_id, user_id))
178    }
179    async fn list_team_members(&self, team_id: &str) -> AuthResult<Vec<TeamMember>> {
180        Ok(self
181            .organization_state()?
182            .team_members
183            .values()
184            .filter(|row| row.team_id == team_id)
185            .take(self.find_many_limit)
186            .cloned()
187            .collect())
188    }
189    async fn list_user_teams(&self, user_id: &str) -> AuthResult<Vec<Team>> {
190        let state = self.organization_state()?;
191        Ok(state
192            .team_members
193            .values()
194            .filter(|row| row.user_id == user_id)
195            .take(self.find_many_limit)
196            .filter_map(|row| state.teams.get(&row.team_id).cloned())
197            .collect())
198    }
199}