alibi_core/store/stateless/
teams.rs1use 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 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}