Files
Rocketmc/src/routes/v3/teams.rs
Wyatt Verchere a70df067bc Misc v3 linear tasks (#767)
* v3_reroute 404 error

* hash change

* fixed issue with error conversion

* added new model confirmation tests
+ title name change

* renaming, fields

* owner; test changes

* clippy prepare

* fmt

* merge fixes

* clippy

* working merge

* revs

* merge fixes
2023-12-01 20:15:00 -07:00

937 lines
32 KiB
Rust

use crate::auth::{get_user_from_headers, is_authorized};
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::team_item::TeamAssociationId;
use crate::database::models::{Organization, Team, TeamMember, User};
use crate::database::redis::RedisPool;
use crate::database::Project;
use crate::models::notifications::NotificationBody;
use crate::models::pats::Scopes;
use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId};
use crate::models::users::UserId;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::{web, HttpRequest, HttpResponse};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("teams", web::get().to(teams_get));
cfg.service(
web::scope("team")
.route("{id}/members", web::get().to(team_members_get))
.route("{id}/members/{user_id}", web::patch().to(edit_team_member))
.route(
"{id}/members/{user_id}",
web::delete().to(remove_team_member),
)
.route("{id}/members", web::post().to(add_team_member))
.route("{id}/join", web::post().to(join_team))
.route("{id}/owner", web::patch().to(transfer_ownership)),
);
}
// Returns all members of a project,
// including the team members of the project's team, but
// also the members of the organization's team if the project is associated with an organization
// (Unlike team_members_get_project, which only returns the members of the project's team)
pub async fn team_members_get_project(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let project_data = crate::database::models::Project::get(&string, &**pool, &redis).await?;
if let Some(project) = project_data {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await
.map(|x| x.1)
.ok();
if !is_authorized(&project.inner, &current_user, &pool).await? {
return Err(ApiError::NotFound);
}
let mut members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
let mut member_user_ids = members_data.iter().map(|x| x.user_id).collect::<Vec<_>>();
// Adds the organization's team members to the list of members, if the project is associated with an organization
if let Some(oid) = project.inner.organization_id {
let organization_data = Organization::get_id(oid, &**pool, &redis).await?;
if let Some(organization_data) = organization_data {
let org_team =
TeamMember::get_from_team_full(organization_data.team_id, &**pool, &redis)
.await?;
for member in org_team {
if !member_user_ids.contains(&member.user_id) {
member_user_ids.push(member.user_id);
members_data.push(member);
}
}
}
}
let users =
crate::database::models::User::get_many_ids(&member_user_ids, &**pool, &redis).await?;
let user_id = current_user.as_ref().map(|x| x.id.into());
let logged_in = current_user
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| {
logged_in
|| x.accepted
|| user_id
.map(|y: crate::database::models::UserId| y == x.user_id)
.unwrap_or(false)
})
.flat_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|user| {
crate::models::teams::TeamMember::from(data, user.clone(), !logged_in)
})
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
} else {
Err(ApiError::NotFound)
}
}
pub async fn team_members_get_organization(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let organization_data =
crate::database::models::Organization::get(&string, &**pool, &redis).await?;
if let Some(organization) = organization_data {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::ORGANIZATION_READ]),
)
.await
.map(|x| x.1)
.ok();
let members_data =
TeamMember::get_from_team_full(organization.team_id, &**pool, &redis).await?;
let users = crate::database::models::User::get_many_ids(
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let user_id = current_user.as_ref().map(|x| x.id.into());
let logged_in = current_user
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| {
logged_in
|| x.accepted
|| user_id
.map(|y: crate::database::models::UserId| y == x.user_id)
.unwrap_or(false)
})
.flat_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|user| {
crate::models::teams::TeamMember::from(data, user.clone(), !logged_in)
})
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
} else {
Err(ApiError::NotFound)
}
}
// Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project)
pub async fn team_members_get(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let members_data = TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?;
let users = crate::database::models::User::get_many_ids(
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await
.map(|x| x.1)
.ok();
let user_id = current_user.as_ref().map(|x| x.id.into());
let logged_in = current_user
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| {
logged_in
|| x.accepted
|| user_id
.map(|y: crate::database::models::UserId| y == x.user_id)
.unwrap_or(false)
})
.flat_map(|data| {
users
.iter()
.find(|x| x.id == data.user_id)
.map(|user| crate::models::teams::TeamMember::from(data, user.clone(), !logged_in))
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
}
#[derive(Serialize, Deserialize)]
pub struct TeamIds {
pub ids: String,
}
pub async fn teams_get(
req: HttpRequest,
web::Query(ids): web::Query<TeamIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
use itertools::Itertools;
let team_ids = serde_json::from_str::<Vec<TeamId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<crate::database::models::ids::TeamId>>();
let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?;
let users = crate::database::models::User::get_many_ids(
&teams_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await
.map(|x| x.1)
.ok();
let teams_groups = teams_data.into_iter().group_by(|data| data.team_id.0);
let mut teams: Vec<Vec<crate::models::teams::TeamMember>> = vec![];
for (_, member_data) in &teams_groups {
let members = member_data.collect::<Vec<_>>();
let logged_in = current_user
.as_ref()
.and_then(|user| {
members
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members = members
.into_iter()
.filter(|x| logged_in || x.accepted)
.flat_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|user| {
crate::models::teams::TeamMember::from(data, user.clone(), !logged_in)
})
});
teams.push(team_members.collect());
}
Ok(HttpResponse::Ok().json(teams))
}
pub async fn join_team(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let member =
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
if let Some(member) = member {
if member.accepted {
return Err(ApiError::InvalidInput(
"You are already a member of this team".to_string(),
));
}
let mut transaction = pool.begin().await?;
// Edit Team Member to set Accepted to True
TeamMember::edit_team_member(
team_id,
current_user.id.into(),
None,
None,
None,
Some(true),
None,
None,
None,
&mut transaction,
)
.await?;
User::clear_project_cache(&[current_user.id.into()], &redis).await?;
TeamMember::clear_cache(team_id, &redis).await?;
transaction.commit().await?;
} else {
return Err(ApiError::InvalidInput(
"There is no pending request from this team".to_string(),
));
}
Ok(HttpResponse::NoContent().body(""))
}
fn default_role() -> String {
"Member".to_string()
}
fn default_ordering() -> i64 {
0
}
#[derive(Serialize, Deserialize, Clone)]
pub struct NewTeamMember {
pub user_id: UserId,
#[serde(default = "default_role")]
pub role: String,
#[serde(default)]
pub permissions: ProjectPermissions,
#[serde(default)]
pub organization_permissions: Option<OrganizationPermissions>,
#[serde(default)]
#[serde(with = "rust_decimal::serde::float")]
pub payouts_split: Decimal,
#[serde(default = "default_ordering")]
pub ordering: i64,
}
pub async fn add_team_member(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_member: web::Json<NewTeamMember>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let team_association = Team::get_association(team_id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?;
match team_association {
// If team is associated with a project, check if they have permissions to invite users to that project
TeamAssociationId::Project(pid) => {
let organization =
Organization::get_associated_organization_project_id(pid, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member,
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this team".to_string(),
));
}
if !permissions.contains(new_member.permissions) {
return Err(ApiError::InvalidInput(
"The new member has permissions that you don't have".to_string(),
));
}
if new_member.organization_permissions.is_some() {
return Err(ApiError::InvalidInput(
"The organization permissions of a project team member cannot be set"
.to_string(),
));
}
}
// If team is associated with an organization, check if they have permissions to invite users to that organization
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
if !organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this organization".to_string(),
));
}
if !organization_permissions
.contains(new_member.organization_permissions.unwrap_or_default())
{
return Err(ApiError::InvalidInput(
"The new member has organization permissions that you don't have".to_string(),
));
}
if !organization_permissions
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS)
&& !new_member.permissions.is_empty()
{
return Err(ApiError::CustomAuthentication(
"You do not have permission to give this user default project permissions. Ensure 'permissions' is set if it is not, and empty (0)."
.to_string(),
));
}
}
}
if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
}
let request =
TeamMember::get_from_user_id_pending(team_id, new_member.user_id.into(), &**pool).await?;
if let Some(req) = request {
if req.accepted {
return Err(ApiError::InvalidInput(
"The user is already a member of that team".to_string(),
));
} else {
return Err(ApiError::InvalidInput(
"There is already a pending member request for this user".to_string(),
));
}
}
crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis)
.await?
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
TeamMember {
id: new_id,
team_id,
user_id: new_member.user_id.into(),
role: new_member.role.clone(),
is_owner: false, // Cannot just create an owner
permissions: new_member.permissions,
organization_permissions: new_member.organization_permissions,
accepted: false,
payouts_split: new_member.payouts_split,
ordering: new_member.ordering,
}
.insert(&mut transaction)
.await?;
match team_association {
TeamAssociationId::Project(pid) => {
NotificationBuilder {
body: NotificationBody::TeamInvite {
project_id: pid.into(),
team_id: team_id.into(),
invited_by: current_user.id,
role: new_member.role.clone(),
},
}
.insert(new_member.user_id.into(), &mut transaction, &redis)
.await?;
}
TeamAssociationId::Organization(oid) => {
NotificationBuilder {
body: NotificationBody::OrganizationInvite {
organization_id: oid.into(),
team_id: team_id.into(),
invited_by: current_user.id,
role: new_member.role.clone(),
},
}
.insert(new_member.user_id.into(), &mut transaction, &redis)
.await?;
}
}
TeamMember::clear_cache(team_id, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[derive(Serialize, Deserialize, Clone)]
pub struct EditTeamMember {
pub permissions: Option<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
pub role: Option<String>,
pub payouts_split: Option<Decimal>,
pub ordering: Option<i64>,
}
pub async fn edit_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
edit_member: web::Json<EditTeamMember>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let team_association = Team::get_association(id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
let mut transaction = pool.begin().await?;
if edit_member_db.is_owner && edit_member.permissions.is_some() {
return Err(ApiError::InvalidInput(
"The owner's permission's in a team cannot be edited".to_string(),
));
}
match team_association {
TeamAssociationId::Project(project_id) => {
let organization =
Organization::get_associated_organization_project_id(project_id, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member.clone(),
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
));
}
if let Some(new_permissions) = edit_member.permissions {
if !permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have".to_string(),
));
}
}
if edit_member.organization_permissions.is_some() {
return Err(ApiError::InvalidInput(
"The organization permissions of a project team member cannot be edited"
.to_string(),
));
}
}
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
));
}
if let Some(new_permissions) = edit_member.organization_permissions {
if !organization_permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new organization permissions have permissions that you don't have"
.to_string(),
));
}
}
if edit_member.permissions.is_some()
&& !organization_permissions
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS)
{
return Err(ApiError::CustomAuthentication(
"You do not have permission to give this user default project permissions."
.to_string(),
));
}
}
}
if let Some(payouts_split) = edit_member.payouts_split {
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
}
}
TeamMember::edit_team_member(
id,
user_id,
edit_member.permissions,
edit_member.organization_permissions,
edit_member.role.clone(),
None,
edit_member.payouts_split,
edit_member.ordering,
None,
&mut transaction,
)
.await?;
TeamMember::clear_cache(id, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[derive(Deserialize)]
pub struct TransferOwnership {
pub user_id: UserId,
}
pub async fn transfer_ownership(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_owner: web::Json<TransferOwnership>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
// Forbid transferring ownership of a project team that is owned by an organization
// These are owned by the organization owner, and must be removed from the organization first
let pid = Team::get_association(id.into(), &**pool).await?;
if let Some(TeamAssociationId::Project(pid)) = pid {
let result = Project::get_id(pid, &**pool, &redis).await?;
if let Some(project_item) = result {
if project_item.inner.organization_id.is_some() {
return Err(ApiError::InvalidInput(
"You cannot transfer ownership of a project team that is owend by an organization"
.to_string(),
));
}
}
}
if !current_user.role.is_admin() {
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
if !member.is_owner {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit the ownership of this team".to_string(),
));
}
}
let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The new owner specified does not exist".to_string())
})?;
if !new_member.accepted {
return Err(ApiError::InvalidInput(
"You can only transfer ownership to members who are currently in your team".to_string(),
));
}
let mut transaction = pool.begin().await?;
// The following are the only places new_is_owner is modified.
TeamMember::edit_team_member(
id.into(),
current_user.id.into(),
None,
None,
None,
None,
None,
None,
Some(false),
&mut transaction,
)
.await?;
TeamMember::edit_team_member(
id.into(),
new_owner.user_id.into(),
Some(ProjectPermissions::all()),
Some(OrganizationPermissions::all()),
None,
None,
None,
None,
Some(true),
&mut transaction,
)
.await?;
TeamMember::clear_cache(id.into(), &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
pub async fn remove_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_WRITE]),
)
.await?
.1;
let team_association = Team::get_association(id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
if let Some(delete_member) = delete_member {
if delete_member.is_owner {
// The owner cannot be removed from a team
return Err(ApiError::CustomAuthentication(
"The owner can't be removed from a team".to_string(),
));
}
let mut transaction = pool.begin().await?;
// Organization attached to a project this team is attached to
match team_association {
TeamAssociationId::Project(pid) => {
let organization =
Organization::get_associated_organization_project_id(pid, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(
organization.team_id,
current_user.id.into(),
&**pool,
)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member,
&organization_team_member,
)
.unwrap_or_default();
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::REMOVE_MEMBER)
// true as if the permission exists, but the member does not, they are part of an org
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this team"
.to_string(),
));
}
} else if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::MANAGE_INVITES)
// true as if the permission exists, but the member does not, they are part of an org
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite".to_string(),
));
}
}
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
// Organization teams requires a TeamMember, so we can 'unwrap'
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if Some(delete_member.user_id) == member.map(|m| m.user_id)
|| organization_permissions.contains(OrganizationPermissions::REMOVE_MEMBER)
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this organization"
.to_string(),
));
}
} else if Some(delete_member.user_id) == member.map(|m| m.user_id)
|| organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES)
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel an organization invite".to_string(),
));
}
}
}
TeamMember::clear_cache(id, &redis).await?;
User::clear_project_cache(&[delete_member.user_id], &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::NotFound)
}
}