1use super::entities::{team, team_member};
2use super::{SqlxStore, lock_exclusive};
3use crate::error::record_not_updated;
4use crate::model::{self, ActiveRow, SqlxModel};
5use crate::pool::{Engine, Exec, SqlxTransaction};
6use crate::schema::{AuthSchema, SqlxUserModel};
7use crate::sql::Sql;
8use alibi_core::error::{AuthError, AuthResult};
9use alibi_core::store::{TeamStore, team_membership_key};
10use alibi_core::types::{AddTeamMemberResult, CreateTeam, Team, TeamMember, UpdateTeam};
11use async_trait::async_trait;
12use chrono::Utc;
13use uuid::Uuid;
14
15fn team_member_lookup(exec: Exec<'_>, team_id: &str, user_id: &str) -> Sql {
16 let mut sql = model::select_model::<team_member::Model>(exec);
17 sql.push(" WHERE ");
18 sql.compare(team_member::Model::TABLE, "teamId", " = ", team_id);
19 sql.push(" AND ");
20 sql.compare(team_member::Model::TABLE, "userId", " = ", user_id);
21 model::limit_one(&mut sql);
22 sql
23}
24
25impl<S> SqlxStore<S>
26where
27 S: AuthSchema,
28 S::User: SqlxUserModel,
29{
30 pub(super) async fn get_team_with_connection(
31 &self,
32 exec: Exec<'_>,
33 organization_id: Option<&str>,
34 team_id: &str,
35 ) -> AuthResult<Option<Team>> {
36 let mut sql = model::by_id::<team::Model>(exec, team_id);
37 if let Some(org) = organization_id {
38 sql.push(" AND ");
39 sql.compare(team::Model::TABLE, "organizationId", " = ", org);
40 }
41 model::limit_one(&mut sql);
42 Ok(exec
43 .fetch_optional::<team::Model>(sql)
44 .await?
45 .map(Into::into))
46 }
47
48 pub(super) async fn add_team_member_in_tx(
49 &self,
50 tx: &SqlxTransaction,
51 team_id: &str,
52 user_id: &str,
53 maximum: Option<f64>,
54 ) -> AuthResult<AddTeamMemberResult> {
55 let exec = Exec::tx(tx);
56 if super::users::find_user_by_id::<S::User>(exec, user_id, super::users::Lock::Shared)
57 .await?
58 .is_none()
59 {
60 return Err(AuthError::UserNotFound);
61 }
62 let mut room = model::by_id::<team::Model>(exec, team_id);
63 model::limit_one(&mut room);
64 lock_exclusive(&mut room);
65 let room = exec
66 .fetch_optional::<team::Model>(room)
67 .await?
68 .ok_or_else(|| AuthError::bad_request("Team not found"))?;
69 if let Some(member) = exec
70 .fetch_optional::<team_member::Model>(team_member_lookup(exec, team_id, user_id))
71 .await?
72 {
73 return Ok(AddTeamMemberResult::Existing(member.into()));
74 }
75 let mut count = Sql::with(exec.engine(), "SELECT COUNT(*) FROM ");
76 count.ident(team_member::Model::TABLE);
77 count.push(" WHERE ");
78 count.compare(team_member::Model::TABLE, "teamId", " = ", team_id);
79 let count = u64::try_from(exec.fetch_scalar::<i64>(count).await?.unwrap_or_default())
80 .unwrap_or_default();
81 let reserved_count = i64::try_from(count)
85 .map_err(|_error| AuthError::internal("Team membership count overflow"))?
86 .max(room.member_count);
87 let mut active = room.into_active();
88 active.set("memberCount", reserved_count);
89 _ = model::update::<team::Model>(exec, &active)
90 .await?
91 .ok_or_else(record_not_updated)?;
92 if let Some(maximum) = maximum {
93 let mut seat = Sql::with(exec.engine(), "SELECT COUNT(*) FROM ");
94 seat.ident(team::Model::TABLE);
95 seat.push(" WHERE ");
96 seat.compare(team::Model::TABLE, "id", " = ", team_id);
97 seat.push(" AND ");
98 if exec.engine() == Engine::Postgres {
99 seat.column(team::Model::TABLE, "memberCount");
102 seat.push(" < ");
103 seat.bind(ryu_js::Buffer::new().format(maximum).to_owned());
104 seat.push("::bigint");
105 } else {
106 seat.compare(team::Model::TABLE, "memberCount", " < ", maximum);
107 }
108 if exec.fetch_scalar::<i64>(seat).await?.unwrap_or_default() == 0 {
109 return Ok(AddTeamMemberResult::LimitReached);
110 }
111 }
112 let key = team_membership_key(team_id, user_id)?;
113 let mut member = ActiveRow::new();
114 member.set(
115 "id",
116 self.generated_id(exec, "teamMember", "teamMember", "id")
117 .await?
118 .unwrap_or_else(|| Uuid::new_v4().to_string()),
119 );
120 member.set("teamId", team_id);
121 member.set("userId", user_id);
122 member.set("membershipKey", Some(key));
123 member.set("createdAt", Utc::now());
124 let member = model::insert::<team_member::Model>(exec, &member).await?;
125 active.set(
126 "memberCount",
127 reserved_count
128 .checked_add(1)
129 .ok_or_else(|| AuthError::internal("Team membership count overflow"))?,
130 );
131 _ = model::update::<team::Model>(exec, &active)
132 .await?
133 .ok_or_else(record_not_updated)?;
134 Ok(AddTeamMemberResult::Added(member.into()))
135 }
136}
137
138#[async_trait]
139impl<S> TeamStore for SqlxStore<S>
140where
141 S: AuthSchema,
142 S::User: SqlxUserModel,
143{
144 async fn create_team(&self, data: CreateTeam) -> AuthResult<Team> {
145 let now = data.updated_at.unwrap_or_else(Utc::now);
146 let mut active = ActiveRow::new();
147 active.set(
148 "id",
149 self.generated_id(self.exec(), "team", "team", "id")
150 .await?
151 .unwrap_or_else(|| Uuid::new_v4().to_string()),
152 );
153 active.set("name", data.name);
154 active.set("organizationId", data.organization_id);
155 active.set("memberCount", 0_i64);
156 active.set("createdAt", now);
157 active.set("updatedAt", data.updated_at);
158 model::insert::<team::Model>(self.exec(), &active)
159 .await
160 .map(Into::into)
161 }
162 async fn get_team(
163 &self,
164 organization_id: Option<&str>,
165 team_id: &str,
166 ) -> AuthResult<Option<Team>> {
167 self.get_team_with_connection(self.exec(), organization_id, team_id)
168 .await
169 }
170 async fn list_teams(&self, organization_id: &str) -> AuthResult<Vec<Team>> {
171 let mut sql = model::select_model::<team::Model>(self.exec());
172 sql.push(" WHERE ");
173 sql.compare(team::Model::TABLE, "organizationId", " = ", organization_id);
174 sql.push(" LIMIT ");
175 sql.bind(self.find_many_limit());
176 Ok(self
177 .exec()
178 .fetch_all::<team::Model>(sql)
179 .await?
180 .into_iter()
181 .map(Into::into)
182 .collect())
183 }
184 async fn update_team(
185 &self,
186 organization_id: &str,
187 team_id: &str,
188 update: UpdateTeam,
189 ) -> AuthResult<Team> {
190 let mut sql = model::by_id::<team::Model>(self.exec(), team_id);
191 sql.push(" AND ");
192 sql.compare(team::Model::TABLE, "organizationId", " = ", organization_id);
193 model::limit_one(&mut sql);
194 let model = self
195 .exec()
196 .fetch_optional::<team::Model>(sql)
197 .await?
198 .ok_or_else(|| AuthError::bad_request("Team not found"))?;
199 let mut active = model.into_active();
200 if let Some(name) = update.name {
201 active.set("name", name);
202 }
203 active.set("updatedAt", Some(Utc::now()));
204 model::update::<team::Model>(self.exec(), &active)
205 .await?
206 .map(Into::into)
207 .ok_or_else(record_not_updated)
208 }
209 async fn delete_team(&self, organization_id: &str, team_id: &str) -> AuthResult<bool> {
210 let (organization_id, team_id) = (organization_id.to_owned(), team_id.to_owned());
211 self.in_transaction(true, async move |tx| {
212 let exec = Exec::tx(tx);
213 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
214 delete.ident(team::Model::TABLE);
215 delete.push(" WHERE ");
216 delete.compare(team::Model::TABLE, "id", " = ", team_id.as_str());
217 delete.push(" AND ");
218 delete.compare(
219 team::Model::TABLE,
220 "organizationId",
221 " = ",
222 organization_id.as_str(),
223 );
224 if exec.execute(delete).await? == 0 {
225 return Ok(false);
226 }
227 let mut members = Sql::with(exec.engine(), "DELETE FROM ");
228 members.ident(team_member::Model::TABLE);
229 members.push(" WHERE ");
230 members.compare(team_member::Model::TABLE, "teamId", " = ", team_id.as_str());
231 _ = exec.execute(members).await?;
232 let mut pending = self.organization_models.invitation.select(exec);
233 pending.push(" WHERE ");
234 self.organization_models.invitation.compare(
235 &mut pending,
236 "organizationId",
237 " = ",
238 organization_id.as_str(),
239 )?;
240 pending.push(" AND ");
241 self.organization_models.invitation.compare(
242 &mut pending,
243 "status",
244 " = ",
245 "pending",
246 )?;
247 pending.push(" AND ");
248 self.organization_models.invitation.compare(
249 &mut pending,
250 "expiresAt",
251 " > ",
252 Utc::now(),
253 )?;
254 for invite in self
255 .organization_models
256 .invitation
257 .fetch_all(exec, pending)
258 .await?
259 {
260 let Some(ids) = invite.team_id.as_deref() else {
261 continue;
262 };
263 if !ids.split(',').any(|id| id == team_id) {
264 continue;
265 }
266 let remaining = ids
267 .split(',')
268 .filter(|id| *id != team_id)
269 .collect::<Vec<_>>()
270 .join(",");
271 let mut active = invite.into_active();
272 active.set("teamId", (!remaining.is_empty()).then_some(remaining));
273 _ = self
274 .organization_models
275 .invitation
276 .update(exec, &active)
277 .await?
278 .ok_or_else(record_not_updated)?;
279 }
280 Ok(true)
281 })
282 .await
283 }
284 async fn get_team_member(
285 &self,
286 team_id: &str,
287 user_id: &str,
288 ) -> AuthResult<Option<TeamMember>> {
289 Ok(self
290 .exec()
291 .fetch_optional::<team_member::Model>(team_member_lookup(self.exec(), team_id, user_id))
292 .await?
293 .map(Into::into))
294 }
295 async fn add_team_member(
296 &self,
297 team_id: &str,
298 user_id: &str,
299 maximum: Option<f64>,
300 ) -> AuthResult<AddTeamMemberResult> {
301 let tx = self.begin(true).await?;
302 let result = self
303 .add_team_member_in_tx(&tx, team_id, user_id, maximum)
304 .await?;
305 tx.commit().await?;
306 Ok(result)
307 }
308 async fn remove_team_member(&self, team_id: &str, user_id: &str) -> AuthResult<usize> {
309 let (team_id, user_id) = (team_id.to_owned(), user_id.to_owned());
310 let removed = self
311 .in_transaction(true, async move |tx| {
312 let exec = Exec::tx(tx);
313 let mut room = model::by_id::<team::Model>(exec, team_id.as_str());
314 model::limit_one(&mut room);
315 lock_exclusive(&mut room);
316 _ = exec.fetch_optional::<team::Model>(room).await?;
317 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
318 delete.ident(team_member::Model::TABLE);
319 delete.push(" WHERE ");
320 delete.compare(team_member::Model::TABLE, "teamId", " = ", team_id.as_str());
321 delete.push(" AND ");
322 delete.compare(team_member::Model::TABLE, "userId", " = ", user_id.as_str());
323 let removed = exec.execute(delete).await?;
324 let count = i64::try_from(removed)
325 .map_err(|_error| AuthError::internal("Team membership count overflow"))?;
326 if count > 0 {
327 release_seats(exec, &team_id, count).await?;
328 }
329 Ok(removed)
330 })
331 .await?;
332 usize::try_from(removed)
333 .map_err(|_error| AuthError::internal("Team membership count overflow"))
334 }
335 async fn list_team_members(&self, team_id: &str) -> AuthResult<Vec<TeamMember>> {
336 let mut sql = model::select_model::<team_member::Model>(self.exec());
337 sql.push(" WHERE ");
338 sql.compare(team_member::Model::TABLE, "teamId", " = ", team_id);
339 sql.push(" LIMIT ");
340 sql.bind(self.find_many_limit());
341 Ok(self
342 .exec()
343 .fetch_all::<team_member::Model>(sql)
344 .await?
345 .into_iter()
346 .map(Into::into)
347 .collect())
348 }
349 async fn list_user_teams(&self, user_id: &str) -> AuthResult<Vec<Team>> {
350 let mut sql = model::select_model::<team_member::Model>(self.exec());
351 sql.push(" WHERE ");
352 sql.compare(team_member::Model::TABLE, "userId", " = ", user_id);
353 sql.push(" LIMIT ");
354 sql.bind(self.find_many_limit());
355 let memberships: Vec<team_member::Model> = self.exec().fetch_all(sql).await?;
356 if memberships.is_empty() {
357 return Ok(Vec::new());
358 }
359 let mut rooms = model::select_model::<team::Model>(self.exec());
360 rooms.push(" WHERE ");
361 rooms.column(team::Model::TABLE, "id");
362 rooms.push(" IN ");
363 rooms.bind_list(memberships.iter().map(|row| row.team_id.clone()));
364 let rooms = self
365 .exec()
366 .fetch_all::<team::Model>(rooms)
367 .await?
368 .into_iter()
369 .map(|row| (row.id.clone(), row))
370 .collect::<std::collections::HashMap<_, _>>();
371 Ok(memberships
374 .into_iter()
375 .filter_map(|membership| rooms.get(&membership.team_id).cloned().map(Into::into))
376 .collect())
377 }
378}
379
380async fn release_seats(exec: Exec<'_>, team_id: &str, count: i64) -> AuthResult<()> {
382 let mut sql = Sql::with(exec.engine(), "UPDATE ");
383 sql.ident(team::Model::TABLE);
384 sql.push(" SET ");
385 sql.ident("memberCount");
386 sql.push(" = ");
387 sql.ident("memberCount");
388 sql.push(" - ");
389 sql.bind(count);
390 sql.push(" WHERE ");
391 sql.compare(team::Model::TABLE, "id", " = ", team_id);
392 sql.push(" AND ");
393 sql.compare(team::Model::TABLE, "memberCount", " >= ", count);
394 _ = exec.execute(sql).await?;
395 Ok(())
396}
397
398pub(super) async fn remove_owned_team_members(
400 tx: &SqlxTransaction,
401 user_id: &str,
402 organization_id: Option<&str>,
403) -> AuthResult<()> {
404 let exec = Exec::tx(tx);
405 if !super::migrator::has_table(exec, "teamMember").await? {
406 return Ok(());
407 }
408 let mut teams = model::select_model::<team::Model>(exec);
409 if let Some(org) = organization_id {
410 teams.push(" WHERE ");
411 teams.compare(team::Model::TABLE, "organizationId", " = ", org);
412 }
413 teams.push(" ORDER BY ");
414 teams.column(team::Model::TABLE, "id");
415 teams.push(" ASC");
416 lock_exclusive(&mut teams);
417 let rooms = exec.fetch_all::<team::Model>(teams).await?;
418 release_owned_team_members(tx, user_id, rooms).await
419}
420
421pub(super) async fn release_owned_team_members(
423 tx: &SqlxTransaction,
424 user_id: &str,
425 rooms: Vec<team::Model>,
426) -> AuthResult<()> {
427 let exec = Exec::tx(tx);
428 for room in rooms {
429 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
430 delete.ident(team_member::Model::TABLE);
431 delete.push(" WHERE ");
432 delete.compare(team_member::Model::TABLE, "teamId", " = ", room.id.as_str());
433 delete.push(" AND ");
434 delete.compare(team_member::Model::TABLE, "userId", " = ", user_id);
435 let deleted = exec.execute(delete).await?;
436 if deleted > 0 {
437 let count = i64::try_from(deleted)
438 .map_err(|_error| AuthError::internal("Team membership count overflow"))?;
439 release_seats(exec, &room.id, count).await?;
440 }
441 }
442 Ok(())
443}