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::{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, "team_id", " = ", team_id);
19 sql.push(" AND ");
20 sql.compare(team_member::Model::TABLE, "user_id", " = ", 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, "organization_id", " = ", 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, "team_id", " = ", 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("member_count", reserved_count);
89 drop(
90 model::update::<team::Model>(exec, &active)
91 .await?
92 .ok_or_else(record_not_updated)?,
93 );
94 if let Some(maximum) = maximum {
95 let mut seat = Sql::with(exec.engine(), "SELECT COUNT(*) FROM ");
96 seat.ident(team::Model::TABLE);
97 seat.push(" WHERE ");
98 seat.compare(team::Model::TABLE, "id", " = ", team_id);
99 seat.push(" AND ");
100 if exec.engine() == crate::pool::Engine::Postgres {
101 seat.column(team::Model::TABLE, "member_count");
104 seat.push(" < ");
105 seat.bind(ryu_js::Buffer::new().format(maximum).to_owned());
106 seat.push("::bigint");
107 } else {
108 seat.compare(team::Model::TABLE, "member_count", " < ", maximum);
109 }
110 if exec.fetch_scalar::<i64>(seat).await?.unwrap_or_default() == 0 {
111 return Ok(AddTeamMemberResult::LimitReached);
112 }
113 }
114 let key = team_membership_key(team_id, user_id)?;
115 let mut member = ActiveRow::new();
116 member.set("id", Uuid::new_v4().to_string());
117 member.set("team_id", team_id);
118 member.set("user_id", user_id);
119 member.set("membership_key", Some(key));
120 member.set("created_at", Utc::now());
121 let member = model::insert::<team_member::Model>(exec, &member).await?;
122 active.set(
123 "member_count",
124 reserved_count
125 .checked_add(1)
126 .ok_or_else(|| AuthError::internal("Team membership count overflow"))?,
127 );
128 drop(
129 model::update::<team::Model>(exec, &active)
130 .await?
131 .ok_or_else(record_not_updated)?,
132 );
133 Ok(AddTeamMemberResult::Added(member.into()))
134 }
135}
136
137#[async_trait]
138impl<S> TeamStore for SqlxStore<S>
139where
140 S: AuthSchema,
141 S::User: SqlxUserModel,
142{
143 async fn create_team(&self, data: CreateTeam) -> AuthResult<Team> {
144 let now = data.updated_at.unwrap_or_else(Utc::now);
145 let mut active = ActiveRow::new();
146 active.set("id", Uuid::new_v4().to_string());
147 active.set("name", data.name);
148 active.set("organization_id", data.organization_id);
149 active.set("member_count", 0_i64);
150 active.set("created_at", now);
151 active.set("updated_at", data.updated_at);
152 model::insert::<team::Model>(self.exec(), &active)
153 .await
154 .map(Into::into)
155 }
156 async fn get_team(
157 &self,
158 organization_id: Option<&str>,
159 team_id: &str,
160 ) -> AuthResult<Option<Team>> {
161 self.get_team_with_connection(self.exec(), organization_id, team_id)
162 .await
163 }
164 async fn list_teams(&self, organization_id: &str) -> AuthResult<Vec<Team>> {
165 let mut sql = model::select_model::<team::Model>(self.exec());
166 sql.push(" WHERE ");
167 sql.compare(
168 team::Model::TABLE,
169 "organization_id",
170 " = ",
171 organization_id,
172 );
173 sql.push(" LIMIT ");
174 sql.bind(self.find_many_limit());
175 Ok(self
176 .exec()
177 .fetch_all::<team::Model>(sql)
178 .await?
179 .into_iter()
180 .map(Into::into)
181 .collect())
182 }
183 async fn update_team(
184 &self,
185 organization_id: &str,
186 team_id: &str,
187 update: UpdateTeam,
188 ) -> AuthResult<Team> {
189 let mut sql = model::by_id::<team::Model>(self.exec(), team_id);
190 sql.push(" AND ");
191 sql.compare(
192 team::Model::TABLE,
193 "organization_id",
194 " = ",
195 organization_id,
196 );
197 model::limit_one(&mut sql);
198 let model = self
199 .exec()
200 .fetch_optional::<team::Model>(sql)
201 .await?
202 .ok_or_else(|| AuthError::bad_request("Team not found"))?;
203 let mut active = model.into_active();
204 if let Some(name) = update.name {
205 active.set("name", name);
206 }
207 active.set("updated_at", Some(Utc::now()));
208 model::update::<team::Model>(self.exec(), &active)
209 .await?
210 .map(Into::into)
211 .ok_or_else(record_not_updated)
212 }
213 async fn delete_team(&self, organization_id: &str, team_id: &str) -> AuthResult<bool> {
214 let (organization_id, team_id) = (organization_id.to_owned(), team_id.to_owned());
215 self.in_transaction(true, async move |tx| {
216 let exec = Exec::Tx(tx);
217 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
218 delete.ident(team::Model::TABLE);
219 delete.push(" WHERE ");
220 delete.compare(team::Model::TABLE, "id", " = ", team_id.as_str());
221 delete.push(" AND ");
222 delete.compare(
223 team::Model::TABLE,
224 "organization_id",
225 " = ",
226 organization_id.as_str(),
227 );
228 if exec.execute(delete).await? == 0 {
229 return Ok(false);
230 }
231 let mut members = Sql::with(exec.engine(), "DELETE FROM ");
232 members.ident(team_member::Model::TABLE);
233 members.push(" WHERE ");
234 members.compare(
235 team_member::Model::TABLE,
236 "team_id",
237 " = ",
238 team_id.as_str(),
239 );
240 _ = exec.execute(members).await?;
241 let mut pending = self.organization_models.invitation.select(exec);
242 pending.push(" WHERE ");
243 self.organization_models.invitation.compare(
244 &mut pending,
245 "organization_id",
246 " = ",
247 organization_id.as_str(),
248 )?;
249 pending.push(" AND ");
250 self.organization_models.invitation.compare(
251 &mut pending,
252 "status",
253 " = ",
254 "pending",
255 )?;
256 pending.push(" AND ");
257 self.organization_models.invitation.compare(
258 &mut pending,
259 "expires_at",
260 " > ",
261 Utc::now(),
262 )?;
263 for invite in self
264 .organization_models
265 .invitation
266 .fetch_all(exec, pending)
267 .await?
268 {
269 let Some(ids) = invite.team_id.as_deref() else {
270 continue;
271 };
272 if !ids.split(',').any(|id| id == team_id) {
273 continue;
274 }
275 let remaining = ids
276 .split(',')
277 .filter(|id| *id != team_id)
278 .collect::<Vec<_>>()
279 .join(",");
280 let mut active = invite.into_active();
281 active.set("team_id", (!remaining.is_empty()).then_some(remaining));
282 drop(
283 self.organization_models
284 .invitation
285 .update(exec, &active)
286 .await?
287 .ok_or_else(record_not_updated)?,
288 );
289 }
290 Ok(true)
291 })
292 .await
293 }
294 async fn get_team_member(
295 &self,
296 team_id: &str,
297 user_id: &str,
298 ) -> AuthResult<Option<TeamMember>> {
299 Ok(self
300 .exec()
301 .fetch_optional::<team_member::Model>(team_member_lookup(self.exec(), team_id, user_id))
302 .await?
303 .map(Into::into))
304 }
305 async fn add_team_member(
306 &self,
307 team_id: &str,
308 user_id: &str,
309 maximum: Option<f64>,
310 ) -> AuthResult<AddTeamMemberResult> {
311 let tx = self.pool().begin(true).await?;
312 let result = self
313 .add_team_member_in_tx(&tx, team_id, user_id, maximum)
314 .await?;
315 tx.commit().await?;
316 Ok(result)
317 }
318 async fn remove_team_member(&self, team_id: &str, user_id: &str) -> AuthResult<usize> {
319 let (team_id, user_id) = (team_id.to_owned(), user_id.to_owned());
320 let removed = self
321 .in_transaction(true, async move |tx| {
322 let exec = Exec::Tx(tx);
323 let mut room = model::by_id::<team::Model>(exec, team_id.as_str());
324 model::limit_one(&mut room);
325 lock_exclusive(&mut room);
326 drop(exec.fetch_optional::<team::Model>(room).await?);
327 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
328 delete.ident(team_member::Model::TABLE);
329 delete.push(" WHERE ");
330 delete.compare(
331 team_member::Model::TABLE,
332 "team_id",
333 " = ",
334 team_id.as_str(),
335 );
336 delete.push(" AND ");
337 delete.compare(
338 team_member::Model::TABLE,
339 "user_id",
340 " = ",
341 user_id.as_str(),
342 );
343 let removed = exec.execute(delete).await?;
344 let count = i64::try_from(removed)
345 .map_err(|_error| AuthError::internal("Team membership count overflow"))?;
346 if count > 0 {
347 release_seats(exec, &team_id, count).await?;
348 }
349 Ok(removed)
350 })
351 .await?;
352 usize::try_from(removed)
353 .map_err(|_error| AuthError::internal("Team membership count overflow"))
354 }
355 async fn list_team_members(&self, team_id: &str) -> AuthResult<Vec<TeamMember>> {
356 let mut sql = model::select_model::<team_member::Model>(self.exec());
357 sql.push(" WHERE ");
358 sql.compare(team_member::Model::TABLE, "team_id", " = ", team_id);
359 sql.push(" LIMIT ");
360 sql.bind(self.find_many_limit());
361 Ok(self
362 .exec()
363 .fetch_all::<team_member::Model>(sql)
364 .await?
365 .into_iter()
366 .map(Into::into)
367 .collect())
368 }
369 async fn list_user_teams(&self, user_id: &str) -> AuthResult<Vec<Team>> {
370 let mut sql = model::select_model::<team_member::Model>(self.exec());
371 sql.push(" WHERE ");
372 sql.compare(team_member::Model::TABLE, "user_id", " = ", user_id);
373 sql.push(" LIMIT ");
374 sql.bind(self.find_many_limit());
375 let memberships: Vec<team_member::Model> = self.exec().fetch_all(sql).await?;
376 if memberships.is_empty() {
377 return Ok(Vec::new());
378 }
379 let mut rooms = model::select_model::<team::Model>(self.exec());
380 rooms.push(" WHERE ");
381 rooms.column(team::Model::TABLE, "id");
382 rooms.push(" IN ");
383 rooms.bind_list(memberships.iter().map(|row| row.team_id.clone()));
384 let rooms = self
385 .exec()
386 .fetch_all::<team::Model>(rooms)
387 .await?
388 .into_iter()
389 .map(|row| (row.id.clone(), row))
390 .collect::<std::collections::HashMap<_, _>>();
391 Ok(memberships
394 .into_iter()
395 .filter_map(|membership| rooms.get(&membership.team_id).cloned().map(Into::into))
396 .collect())
397 }
398}
399
400async fn release_seats(exec: Exec<'_>, team_id: &str, count: i64) -> AuthResult<()> {
402 let mut sql = Sql::with(exec.engine(), "UPDATE ");
403 sql.ident(team::Model::TABLE);
404 sql.push(" SET ");
405 sql.ident("member_count");
406 sql.push(" = ");
407 sql.ident("member_count");
408 sql.push(" - ");
409 sql.bind(count);
410 sql.push(" WHERE ");
411 sql.compare(team::Model::TABLE, "id", " = ", team_id);
412 sql.push(" AND ");
413 sql.compare(team::Model::TABLE, "member_count", " >= ", count);
414 _ = exec.execute(sql).await?;
415 Ok(())
416}
417
418pub(super) async fn remove_owned_team_members(
420 tx: &SqlxTransaction,
421 user_id: &str,
422 organization_id: Option<&str>,
423) -> AuthResult<()> {
424 let exec = Exec::Tx(tx);
425 if !super::migrator::has_table(exec, "team_member").await? {
426 return Ok(());
427 }
428 let mut teams = model::select_model::<team::Model>(exec);
429 if let Some(org) = organization_id {
430 teams.push(" WHERE ");
431 teams.compare(team::Model::TABLE, "organization_id", " = ", org);
432 }
433 teams.push(" ORDER BY ");
434 teams.column(team::Model::TABLE, "id");
435 teams.push(" ASC");
436 lock_exclusive(&mut teams);
437 let rooms = exec.fetch_all::<team::Model>(teams).await?;
438 release_owned_team_members(tx, user_id, rooms).await
439}
440
441pub(super) async fn release_owned_team_members(
443 tx: &SqlxTransaction,
444 user_id: &str,
445 rooms: Vec<team::Model>,
446) -> AuthResult<()> {
447 let exec = Exec::Tx(tx);
448 for room in rooms {
449 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
450 delete.ident(team_member::Model::TABLE);
451 delete.push(" WHERE ");
452 delete.compare(
453 team_member::Model::TABLE,
454 "team_id",
455 " = ",
456 room.id.as_str(),
457 );
458 delete.push(" AND ");
459 delete.compare(team_member::Model::TABLE, "user_id", " = ", user_id);
460 let deleted = exec.execute(delete).await?;
461 if deleted > 0 {
462 let count = i64::try_from(deleted)
463 .map_err(|_error| AuthError::internal("Team membership count overflow"))?;
464 release_seats(exec, &room.id, count).await?;
465 }
466 }
467 Ok(())
468}