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