1use super::super::extensions::{TeamHookContext, TeamLimitContext};
2use super::super::{OrganizationConfig, OrganizationPlugin};
3use super::extension_common::{has_action, org_error, session};
4use alibi_core::entity::AuthUser;
5use alibi_core::error::codes;
6use alibi_core::types::{AddTeamMemberResult, CreateTeam, Team, UpdateTeam};
7use alibi_core::wire::{SessionView, UserView};
8use alibi_core::{AuthContext, AuthRequest, AuthResponse, AuthResult, AuthSchema, HttpMethod};
9use serde::{Deserialize, Serialize};
10use validator::Validate;
11
12#[serde_with::skip_serializing_none]
13#[derive(Debug, Deserialize, Serialize, Validate)]
14#[serde(rename_all = "camelCase")]
15pub struct CreateTeamRequest {
16 pub name: String,
17 pub organization_id: Option<String>,
18}
19
20#[serde_with::skip_serializing_none]
21#[derive(Debug, Deserialize, Serialize, Validate)]
22#[serde(rename_all = "camelCase")]
23pub struct RemoveTeamRequest {
24 pub team_id: String,
25 pub organization_id: Option<String>,
26}
27
28#[serde_with::skip_serializing_none]
29#[derive(Debug, Deserialize, Serialize, Validate)]
30#[serde(rename_all = "camelCase")]
31pub struct UpdateTeamRequest {
32 pub team_id: String,
33 pub data: UpdateTeamData,
34}
35
36#[serde_with::skip_serializing_none]
37#[derive(Debug, Deserialize, Serialize, Validate)]
38#[serde(rename_all = "camelCase")]
39pub struct UpdateTeamData {
40 #[validate(length(min = 1))]
41 pub name: Option<String>,
42 pub organization_id: Option<String>,
43}
44
45#[serde_with::skip_serializing_none]
46#[derive(Debug, Deserialize, Serialize, Validate)]
47#[serde(rename_all = "camelCase")]
48pub struct TeamMemberRequest {
49 pub team_id: String,
50 #[serde(
51 default = "super::super::types::undefined_string",
52 deserialize_with = "super::super::types::deserialize_coercible_string"
53 )]
54 pub user_id: String,
55 pub organization_id: Option<String>,
56}
57
58#[serde_with::skip_serializing_none]
59#[derive(Debug, Deserialize, Serialize, Validate)]
60pub struct SetActiveTeamRequest {
61 #[serde(
62 default,
63 rename = "teamId",
64 deserialize_with = "super::super::types::deserialize_nullable_string_field"
65 )]
66 #[serde(skip_serializing_if = "super::super::types::NullableStringField::is_missing")]
67 pub team_id: super::super::types::NullableStringField,
68}
69
70impl OrganizationPlugin {
71 pub async fn create_team<S: AuthSchema>(
77 &self,
78 ctx: &AuthContext<S>,
79 data: CreateTeam,
80 ) -> AuthResult<Team> {
81 if !self.config.teams.enabled {
82 return Err(alibi_core::AuthError::NotImplemented(
83 "Teams are disabled".to_owned(),
84 ));
85 }
86 create_team_core(data, None, None, ctx, &self.config).await
87 }
88 pub async fn create_team_with_headers<S: AuthSchema>(
95 &self,
96 ctx: &AuthContext<S>,
97 headers: &std::collections::HashMap<String, String>,
98 data: CreateTeam,
99 ) -> AuthResult<Team> {
100 if !self.config.teams.enabled {
101 return Err(alibi_core::AuthError::NotImplemented(
102 "Teams are disabled".to_owned(),
103 ));
104 }
105 let mut resolution = AuthRequest::new(HttpMethod::Post, "/organization/create-team");
106 resolution.headers = headers
107 .iter()
108 .map(|(name, value)| (name.to_ascii_lowercase(), value.clone()))
109 .collect();
110 let (user, current) = session(&resolution, ctx).await?;
111 let user = ctx.user_view(&user);
112 create_team_core(data, Some((&user, ¤t)), None, ctx, &self.config).await
113 }
114
115 pub async fn remove_team_with_headers<S: AuthSchema>(
122 &self,
123 ctx: &AuthContext<S>,
124 headers: &std::collections::HashMap<String, String>,
125 organization_id: &str,
126 team_id: &str,
127 ) -> AuthResult<()> {
128 if !self.config.teams.enabled {
129 return Err(alibi_core::AuthError::NotImplemented(
130 "Teams are disabled".to_owned(),
131 ));
132 }
133 let mut resolution = AuthRequest::new(HttpMethod::Post, "/organization/remove-team");
134 resolution.headers = headers
135 .iter()
136 .map(|(name, value)| (name.to_ascii_lowercase(), value.clone()))
137 .collect();
138 let (user, current) = session(&resolution, ctx).await?;
139 let user = ctx.user_view(&user);
140 remove_team_core(
141 organization_id,
142 team_id,
143 Some((&user, ¤t)),
144 ctx,
145 &self.config,
146 )
147 .await
148 }
149
150 pub async fn remove_team<S: AuthSchema>(
155 &self,
156 ctx: &AuthContext<S>,
157 organization_id: &str,
158 team_id: &str,
159 ) -> AuthResult<()> {
160 if !self.config.teams.enabled {
161 return Err(alibi_core::AuthError::NotImplemented(
162 "Teams are disabled".to_owned(),
163 ));
164 }
165 remove_team_core(organization_id, team_id, None, ctx, &self.config).await
166 }
167}
168
169fn org_id(explicit: Option<&str>, session: Option<&SessionView>) -> AuthResult<String> {
170 explicit
171 .filter(|id| !id.is_empty())
172 .or_else(|| {
173 let s = session?;
174 s.active_organization_id.as_deref()
175 })
176 .map(str::to_owned)
177 .ok_or_else(|| org_error(400, codes::NO_ACTIVE_ORGANIZATION))
178}
179
180async fn hook_context<S: AuthSchema>(
181 ctx: &AuthContext<S>,
182 org_id: &str,
183 user: Option<UserView>,
184) -> AuthResult<TeamHookContext> {
185 let organization = ctx
186 .database
187 .get_organization_by_id(org_id)
188 .await?
189 .ok_or_else(|| org_error(400, codes::ORGANIZATION_NOT_FOUND))?;
190 Ok(TeamHookContext { organization, user })
191}
192
193#[expect(
194 clippy::cast_precision_loss,
195 reason = "Source compares stored counts as ECMAScript Numbers"
196)]
197pub(crate) async fn create_team_core<S: AuthSchema>(
198 mut data: CreateTeam,
199 actor: Option<(&UserView, &SessionView)>,
200 request: Option<&AuthRequest>,
201 ctx: &AuthContext<S>,
202 config: &OrganizationConfig,
203) -> AuthResult<Team> {
204 if let Some((user, _)) = actor {
205 let member = ctx
206 .database
207 .get_member(&data.organization_id, &user.id)
208 .await?
209 .ok_or_else(|| {
210 org_error(
211 403,
212 codes::YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
213 )
214 })?;
215 if !has_action(
216 &member.role,
217 "team",
218 "create",
219 config,
220 ctx,
221 &data.organization_id,
222 )
223 .await?
224 {
225 return Err(org_error(
226 403,
227 codes::YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION,
228 ));
229 }
230 }
231 let limit_ctx = TeamLimitContext {
232 organization_id: data.organization_id.clone(),
233 team_id: None,
234 session: actor.map(|(_, session)| session.clone()),
235 user: actor.map(|(user, _)| user.clone()),
236 request: request.cloned(),
237 };
238 let existing_teams = ctx.database.list_teams(&data.organization_id).await?;
239 let maximum = match &config.teams.limit_resolver {
240 Some(resolver) => resolver.maximum_teams(&limit_ctx).await?,
241 None => config.teams.maximum_teams,
242 };
243 if let Some(maximum) = maximum.filter(|limit| *limit != 0.0 && !limit.is_nan())
244 && existing_teams.len() as f64 >= maximum
245 {
246 return Err(org_error(
247 400,
248 codes::YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS,
249 ));
250 }
251 let hooks = hook_context(
252 ctx,
253 &data.organization_id,
254 actor.map(|(user, _)| user.clone()),
255 )
256 .await?;
257 if request.is_some() {
261 data.updated_at = Some(chrono::Utc::now());
262 }
263 if let Some(callback) = &config.teams.hooks {
264 callback
265 .before_create(&mut data, &hooks)
266 .await
267 .map_err(super::super::extensions::team_callback_error)?;
268 }
269 let team = ctx.database.create_team(data).await?;
270 if let Some(callback) = &config.teams.hooks {
271 callback
272 .after_create(&team, &hooks)
273 .await
274 .map_err(super::super::extensions::team_callback_error)?;
275 }
276 Ok(team)
277}
278
279pub(crate) async fn remove_team_core<S: AuthSchema>(
280 organization_id: &str,
281 team_id: &str,
282 actor: Option<(&UserView, &SessionView)>,
283 ctx: &AuthContext<S>,
284 config: &OrganizationConfig,
285) -> AuthResult<()> {
286 if let Some((user, session)) = actor {
287 let member = ctx
288 .database
289 .get_member(organization_id, &user.id)
290 .await?
291 .ok_or_else(|| org_error(403, codes::YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM))?;
292 if session.active_team_id.as_deref() == Some(team_id) {
293 return Err(org_error(
294 403,
295 codes::YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM,
296 ));
297 }
298 if !has_action(&member.role, "team", "delete", config, ctx, organization_id).await? {
299 return Err(org_error(
300 403,
301 codes::YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION,
302 ));
303 }
304 }
305 let team = ctx
306 .database
307 .get_team(Some(organization_id), team_id)
308 .await?
309 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
310 if !config.teams.allow_removing_all_teams
311 && ctx.database.list_teams(organization_id).await?.len() <= 1
312 {
313 return Err(org_error(400, codes::UNABLE_TO_REMOVE_LAST_TEAM));
314 }
315 let hooks = hook_context(ctx, organization_id, actor.map(|(user, _)| user.clone())).await?;
316 if let Some(callback) = &config.teams.hooks {
317 callback
318 .before_delete(&team, &hooks)
319 .await
320 .map_err(super::super::extensions::team_callback_error)?;
321 }
322 _ = ctx.database.delete_team(organization_id, team_id).await?;
323 if let Some(callback) = &config.teams.hooks {
324 callback
325 .after_delete(&team, &hooks)
326 .await
327 .map_err(super::super::extensions::team_callback_error)?;
328 }
329 Ok(())
330}
331
332pub(crate) async fn handle_team_request<S: AuthSchema>(
333 req: &AuthRequest,
334 ctx: &AuthContext<S>,
335 config: &OrganizationConfig,
336) -> AuthResult<Option<AuthResponse>> {
337 let is_known = matches!(
338 (req.method(), req.path()),
339 (
340 HttpMethod::Post,
341 "/organization/create-team"
342 | "/organization/update-team"
343 | "/organization/remove-team"
344 | "/organization/set-active-team"
345 | "/organization/add-team-member"
346 | "/organization/remove-team-member"
347 ) | (
348 HttpMethod::Get,
349 "/organization/list-teams"
350 | "/organization/list-user-teams"
351 | "/organization/list-team-members"
352 )
353 );
354 if !config.teams.enabled || !is_known {
355 return Ok(None);
356 }
357 let (user, current) = session(req, ctx).await?;
358 team_core(
359 req.method(),
360 req.path(),
361 None,
362 &req.query,
363 user,
364 current,
365 Some(req),
366 true,
367 ctx,
368 config,
369 )
370 .await
371}
372
373#[expect(
374 clippy::too_many_lines,
375 reason = "Keep team endpoint dispatch and each organization ownership check adjacent to its writes"
376)]
377#[expect(
378 clippy::too_many_arguments,
379 reason = "Shared execution keeps logical input, original request metadata, authenticated authority, and installed configuration separate"
380)]
381pub(in crate::organization) async fn team_core<S: AuthSchema>(
382 method: &HttpMethod,
383 path: &str,
384 input: Option<serde_json::Value>,
385 query: &std::collections::HashMap<String, String>,
386 user: alibi_core::AuthenticatedUser<S>,
387 current: SessionView,
388 request: Option<&AuthRequest>,
389 http_input: bool,
390 ctx: &AuthContext<S>,
391 config: &OrganizationConfig,
392) -> AuthResult<Option<AuthResponse>> {
393 if !config.teams.enabled {
394 return Err(org_error(404, codes::TEAMS_DISABLED));
395 }
396 let user_view = ctx.user_view(&user);
397 macro_rules! body {
398 ($ty:ty) => {
399 if let Some(request) = request.filter(|_| http_input) {
400 match alibi_core::validate_request_body::<$ty>(request) {
401 Ok(body) => body,
402 Err(response) => return Ok(Some(response)),
403 }
404 } else {
405 let body: $ty =
406 serde_json::from_value(input.clone().unwrap_or(serde_json::Value::Null))
407 .map_err(|error| crate::endpoint::validation(error.to_string()))?;
408 body.validate()
409 .map_err(|error| crate::endpoint::validation(error.to_string()))?;
410 body
411 }
412 };
413 }
414 let response = match (method, path) {
415 (HttpMethod::Post, "/organization/create-team") => {
416 let body = body!(CreateTeamRequest);
417 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
418 let team = create_team_core(
419 CreateTeam {
420 name: body.name,
421 organization_id: org,
422 updated_at: Some(chrono::Utc::now()),
423 },
424 Some((&user_view, ¤t)),
425 request,
426 ctx,
427 config,
428 )
429 .await?;
430 AuthResponse::json(200, &team)?
431 }
432 (HttpMethod::Post, "/organization/remove-team") => {
433 let body = body!(RemoveTeamRequest);
434 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
435 remove_team_core(
436 &org,
437 &body.team_id,
438 Some((&user_view, ¤t)),
439 ctx,
440 config,
441 )
442 .await?;
443 AuthResponse::json(
444 200,
445 &serde_json::json!({"message":"Team removed successfully."}),
446 )?
447 }
448 (HttpMethod::Post, "/organization/update-team") => {
449 let body = body!(UpdateTeamRequest);
450 if body.data.name.as_deref() == Some("") {
451 return Ok(Some(AuthResponse::json(
452 400,
453 &serde_json::json!({"code":codes::VALIDATION_ERROR,"message":"[body.data.name] Too small: expected string to have >=1 characters"}),
454 )?));
455 }
456 let org = org_id(body.data.organization_id.as_deref(), Some(¤t))?;
457 let member = ctx
458 .database
459 .get_member(&org, user.id().as_ref())
460 .await?
461 .ok_or_else(|| org_error(403, codes::YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM))?;
462 if !has_action(&member.role, "team", "update", config, ctx, &org).await? {
463 return Err(org_error(
464 403,
465 codes::YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
466 ));
467 }
468 let team = ctx
469 .database
470 .get_team(Some(&org), &body.team_id)
471 .await?
472 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
473 let hooks = hook_context(ctx, &org, Some(user_view)).await?;
474 let mut updates = UpdateTeam {
475 name: body.data.name,
476 };
477 if let Some(callback) = &config.teams.hooks {
478 callback
479 .before_update(&team, &mut updates, &hooks)
480 .await
481 .map_err(super::super::extensions::team_callback_error)?;
482 }
483 let team_2 = ctx
484 .database
485 .update_team(&org, &body.team_id, updates)
486 .await?;
487 if let Some(callback) = &config.teams.hooks {
488 callback
489 .after_update(&team_2, &hooks)
490 .await
491 .map_err(super::super::extensions::team_callback_error)?;
492 }
493 AuthResponse::json(200, &team_2)?
494 }
495 (HttpMethod::Get, "/organization/list-teams") => {
496 let org = org_id(
497 query.get("organizationId").map(String::as_str),
498 Some(¤t),
499 )?;
500 if ctx
501 .database
502 .get_member(&org, user.id().as_ref())
503 .await?
504 .is_none()
505 {
506 return Err(org_error(
507 403,
508 codes::YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION,
509 ));
510 }
511 AuthResponse::json(200, &ctx.database.list_teams(&org).await?)?
512 }
513 (HttpMethod::Get, "/organization/list-user-teams") => {
514 let target = query
515 .get("userId")
516 .filter(|id| !id.is_empty())
517 .map_or(user_view.id.as_str(), String::as_str);
518 let explicit = query
519 .get("organizationId")
520 .filter(|id| !id.is_empty())
521 .map(String::as_str);
522 let scope = if target == user_view.id {
523 explicit.map(str::to_owned)
524 } else {
525 Some(org_id(explicit, Some(¤t))?)
526 };
527 if let Some(org) = &scope {
528 let requester = ctx
529 .database
530 .get_member(org, &user_view.id)
531 .await?
532 .ok_or_else(|| {
533 org_error(403, codes::YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION)
534 })?;
535 if target != user_view.id {
536 if !has_action(&requester.role, "member", "update", config, ctx, org).await? {
537 return Err(org_error(
538 403,
539 codes::YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
540 ));
541 }
542 if ctx.database.get_member(org, target).await?.is_none() {
543 return Err(org_error(
544 400,
545 codes::USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
546 ));
547 }
548 }
549 }
550 let mut teams = Vec::new();
551 for team in ctx.database.list_user_teams(target).await? {
552 if scope
553 .as_deref()
554 .is_some_and(|org| team.organization_id != org)
555 {
556 continue;
557 }
558 if ctx
559 .database
560 .get_member(&team.organization_id, target)
561 .await?
562 .is_some()
563 {
564 teams.push(team);
565 }
566 }
567 AuthResponse::json(200, &teams)?
568 }
569 (HttpMethod::Get, "/organization/list-team-members") => {
570 let team_id = query
571 .get("teamId")
572 .filter(|id| !id.is_empty())
573 .map(String::as_str)
574 .or(current.active_team_id.as_deref())
575 .ok_or_else(|| org_error(400, codes::YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM))?;
576 let team = ctx
577 .database
578 .get_team(None, team_id)
579 .await?
580 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
581 if ctx
582 .database
583 .get_member(&team.organization_id, &user_view.id)
584 .await?
585 .is_none()
586 || ctx
587 .database
588 .get_team_member(team_id, &user_view.id)
589 .await?
590 .is_none()
591 {
592 return Err(org_error(400, codes::USER_IS_NOT_A_MEMBER_OF_THE_TEAM));
593 }
594 AuthResponse::json(200, &ctx.database.list_team_members(team_id).await?)?
595 }
596 (HttpMethod::Post, "/organization/set-active-team") => {
597 use super::super::types::NullableStringField;
598 let body = body!(SetActiveTeamRequest);
599 let team_id = match body.team_id {
600 NullableStringField::Null => None,
601 NullableStringField::Missing => current.active_team_id.clone(),
602 NullableStringField::Value(value) => {
603 if value.is_empty() {
604 current.active_team_id.clone()
605 } else {
606 Some(value)
607 }
608 }
609 };
610 if team_id.is_none() {
611 let mut response = AuthResponse::json(200, &serde_json::Value::Null)?;
612 if current.active_team_id.is_some() {
613 let updated = ctx
614 .database
615 .update_session_active_team_record(¤t.token, None)
616 .await?;
617 response.headers.append(
618 "set-cookie",
619 alibi_core::utils::cookie_utils::create_session_cookie(
620 alibi_core::AuthSession::token(&updated),
621 &ctx.config,
622 )?,
623 );
624 }
625 response
626 } else {
627 let team_id = team_id
628 .as_deref()
629 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
630 let org = org_id(None, Some(¤t))?;
631 let team = ctx
632 .database
633 .get_team(Some(&org), team_id)
634 .await?
635 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
636 if ctx
637 .database
638 .get_team_member(team_id, &user_view.id)
639 .await?
640 .is_none()
641 {
642 return Err(org_error(403, codes::USER_IS_NOT_A_MEMBER_OF_THE_TEAM));
643 }
644 let updated = ctx
645 .database
646 .update_session_active_team_record(¤t.token, Some(team_id))
647 .await?;
648 AuthResponse::json(200, &team)?.with_header(
649 "set-cookie",
650 alibi_core::utils::cookie_utils::create_session_cookie(
651 alibi_core::AuthSession::token(&updated),
652 &ctx.config,
653 )?,
654 )
655 }
656 }
657 (
658 HttpMethod::Post,
659 "/organization/add-team-member" | "/organization/remove-team-member",
660 ) => {
661 let body = body!(TeamMemberRequest);
662 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
663 let requester = ctx
664 .database
665 .get_member(&org, &user_view.id)
666 .await?
667 .ok_or_else(|| org_error(400, codes::USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION))?;
668 let add = path.ends_with("/add-team-member");
669 let action = if add { "update" } else { "delete" };
670 if !has_action(&requester.role, "member", action, config, ctx, &org).await? {
671 return Err(org_error(
672 403,
673 if add {
674 codes::YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER
675 } else {
676 codes::YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER
677 },
678 ));
679 }
680 if ctx
681 .database
682 .get_member(&org, &body.user_id)
683 .await?
684 .is_none()
685 {
686 return Err(org_error(
687 400,
688 codes::USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
689 ));
690 }
691 let team = ctx
692 .database
693 .get_team(Some(&org), &body.team_id)
694 .await?
695 .ok_or_else(|| org_error(400, codes::TEAM_NOT_FOUND))?;
696 let target = ctx
697 .database
698 .get_user_by_id(&body.user_id)
699 .await?
700 .ok_or_else(|| alibi_core::AuthError::bad_request("User not found"))?;
701 let target = ctx.user_view(&target);
702 let hooks = hook_context(ctx, &org, Some(user_view)).await?;
703 if add {
704 if let Some(callback) = &config.teams.hooks {
705 callback
706 .before_add_member(&team, &target, &hooks)
707 .await
708 .map_err(super::super::extensions::team_callback_error)?;
709 }
710 let limit_ctx = TeamLimitContext {
711 organization_id: org,
712 team_id: Some(team.id.clone()),
713 session: Some(current),
714 user: hooks.user.clone(),
715 request: None,
716 };
717 let maximum = match &config.teams.limit_resolver {
718 Some(resolver) => resolver.maximum_team_members(&limit_ctx).await?,
719 None => config.teams.maximum_members_per_team,
720 };
721 let member = match ctx
722 .database
723 .add_team_member(&team.id, &body.user_id, maximum)
724 .await?
725 {
726 AddTeamMemberResult::Added(member) | AddTeamMemberResult::Existing(member) => {
727 member
728 }
729 AddTeamMemberResult::LimitReached => {
730 return Err(org_error(403, codes::TEAM_MEMBER_LIMIT_REACHED));
731 }
732 };
733 if let Some(callback) = &config.teams.hooks {
734 callback
735 .after_add_member(&member, &team, &target, &hooks)
736 .await
737 .map_err(super::super::extensions::team_callback_error)?;
738 }
739 AuthResponse::json(200, &member)?
740 } else {
741 let member = ctx
742 .database
743 .get_team_member(&team.id, &body.user_id)
744 .await?
745 .ok_or_else(|| org_error(400, codes::USER_IS_NOT_A_MEMBER_OF_THE_TEAM))?;
746 if let Some(callback) = &config.teams.hooks {
747 callback
748 .before_remove_member(&member, &team, &target, &hooks)
749 .await
750 .map_err(super::super::extensions::team_callback_error)?;
751 }
752 _ = ctx
753 .database
754 .remove_team_member(&team.id, &body.user_id)
755 .await?;
756 if let Some(callback) = &config.teams.hooks {
757 callback
758 .after_remove_member(&member, &team, &target, &hooks)
759 .await
760 .map_err(super::super::extensions::team_callback_error)?;
761 }
762 AuthResponse::json(
763 200,
764 &serde_json::json!({"message":"Team member removed successfully."}),
765 )?
766 }
767 }
768 _ => return Ok(None),
769 };
770 Ok(Some(response))
771}