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(
197 clippy::cast_precision_loss,
198 reason = "Source compares stored counts as ECMAScript Numbers"
199)]
200pub async fn create_team_core<S: AuthSchema>(
201 mut data: CreateTeam,
202 actor: Option<(&UserView, &SessionView)>,
203 request: Option<&AuthRequest>,
204 ctx: &AuthContext<S>,
205 config: &OrganizationConfig,
206) -> AuthResult<Team> {
207 if let Some((user, _)) = actor {
208 let member = ctx
209 .database
210 .get_member(&data.organization_id, &user.id)
211 .await?
212 .ok_or_else(|| {
213 org_error(
214 403,
215 "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION",
216 )
217 })?;
218 if !has_action(
219 &member.role,
220 "team",
221 "create",
222 config,
223 ctx,
224 &data.organization_id,
225 )
226 .await?
227 {
228 return Err(org_error(
229 403,
230 "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION",
231 ));
232 }
233 }
234 let limit_ctx = TeamLimitContext {
235 organization_id: data.organization_id.clone(),
236 team_id: None,
237 session: actor.map(|(_, session)| session.clone()),
238 user: actor.map(|(user, _)| user.clone()),
239 request: request.cloned(),
240 };
241 let existing_teams = ctx.database.list_teams(&data.organization_id).await?;
242 let maximum = match &config.teams.limit_resolver {
243 Some(resolver) => resolver.maximum_teams(&limit_ctx).await?,
244 None => config.teams.maximum_teams,
245 };
246 if let Some(maximum) = maximum.filter(|limit| *limit != 0.0 && !limit.is_nan())
247 && existing_teams.len() as f64 >= maximum
248 {
249 return Err(org_error(
250 400,
251 "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS",
252 ));
253 }
254 let hooks = hook_context(
255 ctx,
256 &data.organization_id,
257 actor.map(|(user, _)| user.clone()),
258 )
259 .await?;
260 if request.is_some() {
264 data.updated_at = Some(chrono::Utc::now());
265 }
266 if let Some(callback) = &config.teams.hooks {
267 callback
268 .before_create(&mut data, &hooks)
269 .await
270 .map_err(super::super::extensions::team_callback_error)?;
271 }
272 let team = ctx.database.create_team(data).await?;
273 if let Some(callback) = &config.teams.hooks {
274 callback
275 .after_create(&team, &hooks)
276 .await
277 .map_err(super::super::extensions::team_callback_error)?;
278 }
279 Ok(team)
280}
281
282pub async fn remove_team_core<S: AuthSchema>(
287 organization_id: &str,
288 team_id: &str,
289 actor: Option<(&UserView, &SessionView)>,
290 ctx: &AuthContext<S>,
291 config: &OrganizationConfig,
292) -> AuthResult<()> {
293 if let Some((user, session)) = actor {
294 let member = ctx
295 .database
296 .get_member(organization_id, &user.id)
297 .await?
298 .ok_or_else(|| org_error(403, "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM"))?;
299 if session.active_team_id.as_deref() == Some(team_id) {
300 return Err(org_error(403, "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM"));
301 }
302 if !has_action(&member.role, "team", "delete", config, ctx, organization_id).await? {
303 return Err(org_error(
304 403,
305 "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION",
306 ));
307 }
308 }
309 let team = ctx
310 .database
311 .get_team(Some(organization_id), team_id)
312 .await?
313 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
314 if !config.teams.allow_removing_all_teams
315 && ctx.database.list_teams(organization_id).await?.len() <= 1
316 {
317 return Err(org_error(400, "UNABLE_TO_REMOVE_LAST_TEAM"));
318 }
319 let hooks = hook_context(ctx, organization_id, actor.map(|(user, _)| user.clone())).await?;
320 if let Some(callback) = &config.teams.hooks {
321 callback
322 .before_delete(&team, &hooks)
323 .await
324 .map_err(super::super::extensions::team_callback_error)?;
325 }
326 let _ignored_delete_team = ctx.database.delete_team(organization_id, team_id).await?;
327 if let Some(callback) = &config.teams.hooks {
328 callback
329 .after_delete(&team, &hooks)
330 .await
331 .map_err(super::super::extensions::team_callback_error)?;
332 }
333 Ok(())
334}
335
336pub async fn handle_team_request<S: AuthSchema>(
341 req: &AuthRequest,
342 ctx: &AuthContext<S>,
343 config: &OrganizationConfig,
344) -> AuthResult<Option<AuthResponse>> {
345 let is_known = matches!(
346 (req.method(), req.path()),
347 (
348 HttpMethod::Post,
349 "/organization/create-team"
350 | "/organization/update-team"
351 | "/organization/remove-team"
352 | "/organization/set-active-team"
353 | "/organization/add-team-member"
354 | "/organization/remove-team-member"
355 ) | (
356 HttpMethod::Get,
357 "/organization/list-teams"
358 | "/organization/list-user-teams"
359 | "/organization/list-team-members"
360 )
361 );
362 if !config.teams.enabled || !is_known {
363 return Ok(None);
364 }
365 let (user, current) = session(req, ctx).await?;
366 team_core(
367 req.method(),
368 req.path(),
369 None,
370 &req.query,
371 user,
372 current,
373 Some(req),
374 true,
375 ctx,
376 config,
377 )
378 .await
379}
380
381#[expect(
382 clippy::too_many_lines,
383 reason = "Keep team endpoint dispatch and each organization ownership check adjacent to its writes"
384)]
385#[expect(
386 clippy::too_many_arguments,
387 reason = "Shared execution keeps logical input, original request metadata, authenticated authority, and installed configuration separate"
388)]
389pub(in crate::plugins::organization) async fn team_core<S: AuthSchema>(
390 method: &HttpMethod,
391 path: &str,
392 input: Option<serde_json::Value>,
393 query: &std::collections::HashMap<String, String>,
394 user: alibi_core::AuthenticatedUser<S>,
395 current: SessionView,
396 request: Option<&AuthRequest>,
397 http_input: bool,
398 ctx: &AuthContext<S>,
399 config: &OrganizationConfig,
400) -> AuthResult<Option<AuthResponse>> {
401 if !config.teams.enabled {
402 return Err(org_error(404, "TEAMS_DISABLED"));
403 }
404 let user_view = ctx.user_view(&user);
405 macro_rules! body {
406 ($ty:ty) => {
407 if let Some(request) = request.filter(|_| http_input) {
408 match alibi_core::validate_request_body::<$ty>(request) {
409 Ok(body) => body,
410 Err(response) => return Ok(Some(response)),
411 }
412 } else {
413 let body: $ty =
414 serde_json::from_value(input.clone().unwrap_or(serde_json::Value::Null))
415 .map_err(|error| crate::plugins::endpoint::validation(error.to_string()))?;
416 body.validate()
417 .map_err(|error| crate::plugins::endpoint::validation(error.to_string()))?;
418 body
419 }
420 };
421 }
422 let response = match (method, path) {
423 (HttpMethod::Post, "/organization/create-team") => {
424 let body = body!(CreateTeamRequest);
425 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
426 let team = create_team_core(
427 CreateTeam {
428 name: body.name,
429 organization_id: org,
430 updated_at: Some(chrono::Utc::now()),
431 },
432 Some((&user_view, ¤t)),
433 request,
434 ctx,
435 config,
436 )
437 .await?;
438 AuthResponse::json(200, &team)?
439 }
440 (HttpMethod::Post, "/organization/remove-team") => {
441 let body = body!(RemoveTeamRequest);
442 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
443 remove_team_core(
444 &org,
445 &body.team_id,
446 Some((&user_view, ¤t)),
447 ctx,
448 config,
449 )
450 .await?;
451 AuthResponse::json(
452 200,
453 &serde_json::json!({"message":"Team removed successfully."}),
454 )?
455 }
456 (HttpMethod::Post, "/organization/update-team") => {
457 let body = body!(UpdateTeamRequest);
458 if body.data.name.as_deref() == Some("") {
459 return Ok(Some(AuthResponse::json(
460 400,
461 &serde_json::json!({"code":"VALIDATION_ERROR","message":"[body.data.name] Too small: expected string to have >=1 characters"}),
462 )?));
463 }
464 let org = org_id(body.data.organization_id.as_deref(), Some(¤t))?;
465 let member = ctx
466 .database
467 .get_member(&org, user.id().as_ref())
468 .await?
469 .ok_or_else(|| org_error(403, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM"))?;
470 if !has_action(&member.role, "team", "update", config, ctx, &org).await? {
471 return Err(org_error(403, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM"));
472 }
473 let team = ctx
474 .database
475 .get_team(Some(&org), &body.team_id)
476 .await?
477 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
478 let hooks = hook_context(ctx, &org, Some(user_view)).await?;
479 let mut updates = UpdateTeam {
480 name: body.data.name,
481 };
482 if let Some(callback) = &config.teams.hooks {
483 callback
484 .before_update(&team, &mut updates, &hooks)
485 .await
486 .map_err(super::super::extensions::team_callback_error)?;
487 }
488 let team_2 = ctx
489 .database
490 .update_team(&org, &body.team_id, updates)
491 .await?;
492 if let Some(callback) = &config.teams.hooks {
493 callback
494 .after_update(&team_2, &hooks)
495 .await
496 .map_err(super::super::extensions::team_callback_error)?;
497 }
498 AuthResponse::json(200, &team_2)?
499 }
500 (HttpMethod::Get, "/organization/list-teams") => {
501 let org = org_id(
502 query.get("organizationId").map(String::as_str),
503 Some(¤t),
504 )?;
505 if ctx
506 .database
507 .get_member(&org, user.id().as_ref())
508 .await?
509 .is_none()
510 {
511 return Err(org_error(
512 403,
513 "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION",
514 ));
515 }
516 AuthResponse::json(200, &ctx.database.list_teams(&org).await?)?
517 }
518 (HttpMethod::Get, "/organization/list-user-teams") => {
519 let target = query
520 .get("userId")
521 .filter(|id| !id.is_empty())
522 .map_or(user_view.id.as_str(), String::as_str);
523 let explicit = query
524 .get("organizationId")
525 .filter(|id| !id.is_empty())
526 .map(String::as_str);
527 let scope = if target == user_view.id {
528 explicit.map(str::to_owned)
529 } else {
530 Some(org_id(explicit, Some(¤t))?)
531 };
532 if let Some(org) = &scope {
533 let requester = ctx
534 .database
535 .get_member(org, &user_view.id)
536 .await?
537 .ok_or_else(|| org_error(403, "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION"))?;
538 if target != user_view.id {
539 if !has_action(&requester.role, "member", "update", config, ctx, org).await? {
540 return Err(org_error(403, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER"));
541 }
542 if ctx.database.get_member(org, target).await?.is_none() {
543 return Err(org_error(400, "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION"));
544 }
545 }
546 }
547 let mut teams = Vec::new();
548 for team in ctx.database.list_user_teams(target).await? {
549 if scope
550 .as_deref()
551 .is_some_and(|org| team.organization_id != org)
552 {
553 continue;
554 }
555 if ctx
556 .database
557 .get_member(&team.organization_id, target)
558 .await?
559 .is_some()
560 {
561 teams.push(team);
562 }
563 }
564 AuthResponse::json(200, &teams)?
565 }
566 (HttpMethod::Get, "/organization/list-team-members") => {
567 let team_id = query
568 .get("teamId")
569 .filter(|id| !id.is_empty())
570 .map(String::as_str)
571 .or(current.active_team_id.as_deref())
572 .ok_or_else(|| org_error(400, "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM"))?;
573 let team = ctx
574 .database
575 .get_team(None, team_id)
576 .await?
577 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
578 if ctx
579 .database
580 .get_member(&team.organization_id, &user_view.id)
581 .await?
582 .is_none()
583 || ctx
584 .database
585 .get_team_member(team_id, &user_view.id)
586 .await?
587 .is_none()
588 {
589 return Err(org_error(400, "USER_IS_NOT_A_MEMBER_OF_THE_TEAM"));
590 }
591 AuthResponse::json(200, &ctx.database.list_team_members(team_id).await?)?
592 }
593 (HttpMethod::Post, "/organization/set-active-team") => {
594 use super::super::types::NullableStringField;
595 let body = body!(SetActiveTeamRequest);
596 let team_id = match body.team_id {
597 NullableStringField::Null => None,
598 NullableStringField::Missing => current.active_team_id.clone(),
599 NullableStringField::Value(value) => {
600 if value.is_empty() {
601 current.active_team_id.clone()
602 } else {
603 Some(value)
604 }
605 }
606 };
607 if team_id.is_none() {
608 let mut response = AuthResponse::json(200, &serde_json::Value::Null)?;
609 if current.active_team_id.is_some() {
610 let updated = ctx
611 .database
612 .update_session_active_team_record(¤t.token, None)
613 .await?;
614 response.headers.append(
615 "set-cookie",
616 alibi_core::utils::cookie_utils::create_session_cookie(
617 alibi_core::AuthSession::token(&updated),
618 &ctx.config,
619 )?,
620 );
621 }
622 response
623 } else {
624 let team_id = team_id
625 .as_deref()
626 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
627 let org = org_id(None, Some(¤t))?;
628 let team = ctx
629 .database
630 .get_team(Some(&org), team_id)
631 .await?
632 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
633 if ctx
634 .database
635 .get_team_member(team_id, &user_view.id)
636 .await?
637 .is_none()
638 {
639 return Err(org_error(403, "USER_IS_NOT_A_MEMBER_OF_THE_TEAM"));
640 }
641 let updated = ctx
642 .database
643 .update_session_active_team_record(¤t.token, Some(team_id))
644 .await?;
645 AuthResponse::json(200, &team)?.with_header(
646 "set-cookie",
647 alibi_core::utils::cookie_utils::create_session_cookie(
648 alibi_core::AuthSession::token(&updated),
649 &ctx.config,
650 )?,
651 )
652 }
653 }
654 (
655 HttpMethod::Post,
656 "/organization/add-team-member" | "/organization/remove-team-member",
657 ) => {
658 let body = body!(TeamMemberRequest);
659 let org = org_id(body.organization_id.as_deref(), Some(¤t))?;
660 let requester = ctx
661 .database
662 .get_member(&org, &user_view.id)
663 .await?
664 .ok_or_else(|| org_error(400, "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION"))?;
665 let add = path.ends_with("/add-team-member");
666 let action = if add { "update" } else { "delete" };
667 if !has_action(&requester.role, "member", action, config, ctx, &org).await? {
668 return Err(org_error(
669 403,
670 if add {
671 "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER"
672 } else {
673 "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER"
674 },
675 ));
676 }
677 if ctx
678 .database
679 .get_member(&org, &body.user_id)
680 .await?
681 .is_none()
682 {
683 return Err(org_error(400, "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION"));
684 }
685 let team = ctx
686 .database
687 .get_team(Some(&org), &body.team_id)
688 .await?
689 .ok_or_else(|| org_error(400, "TEAM_NOT_FOUND"))?;
690 let target = ctx
691 .database
692 .get_user_by_id(&body.user_id)
693 .await?
694 .ok_or_else(|| alibi_core::AuthError::bad_request("User not found"))?;
695 let target = ctx.user_view(&target);
696 let hooks = hook_context(ctx, &org, Some(user_view)).await?;
697 if add {
698 if let Some(callback) = &config.teams.hooks {
699 callback
700 .before_add_member(&team, &target, &hooks)
701 .await
702 .map_err(super::super::extensions::team_callback_error)?;
703 }
704 let limit_ctx = TeamLimitContext {
705 organization_id: org,
706 team_id: Some(team.id.clone()),
707 session: Some(current),
708 user: hooks.user.clone(),
709 request: None,
710 };
711 let maximum = match &config.teams.limit_resolver {
712 Some(resolver) => resolver.maximum_team_members(&limit_ctx).await?,
713 None => config.teams.maximum_members_per_team,
714 };
715 let member = match ctx
716 .database
717 .add_team_member(&team.id, &body.user_id, maximum)
718 .await?
719 {
720 AddTeamMemberResult::Added(member) | AddTeamMemberResult::Existing(member) => {
721 member
722 }
723 AddTeamMemberResult::LimitReached => {
724 return Err(org_error(403, "TEAM_MEMBER_LIMIT_REACHED"));
725 }
726 };
727 if let Some(callback) = &config.teams.hooks {
728 callback
729 .after_add_member(&member, &team, &target, &hooks)
730 .await
731 .map_err(super::super::extensions::team_callback_error)?;
732 }
733 AuthResponse::json(200, &member)?
734 } else {
735 let member = ctx
736 .database
737 .get_team_member(&team.id, &body.user_id)
738 .await?
739 .ok_or_else(|| org_error(400, "USER_IS_NOT_A_MEMBER_OF_THE_TEAM"))?;
740 if let Some(callback) = &config.teams.hooks {
741 callback
742 .before_remove_member(&member, &team, &target, &hooks)
743 .await
744 .map_err(super::super::extensions::team_callback_error)?;
745 }
746 let _ignored_remove_team_member = ctx
747 .database
748 .remove_team_member(&team.id, &body.user_id)
749 .await?;
750 if let Some(callback) = &config.teams.hooks {
751 callback
752 .after_remove_member(&member, &team, &target, &hooks)
753 .await
754 .map_err(super::super::extensions::team_callback_error)?;
755 }
756 AuthResponse::json(
757 200,
758 &serde_json::json!({"message":"Team member removed successfully."}),
759 )?
760 }
761 }
762 _ => return Ok(None),
763 };
764 Ok(Some(response))
765}