You've already forked AstralRinth
forked from didirus/AstralRinth
Organization ownership (#796)
* organization changes * changes * fixes failing test * version changes * removed printlns * add_team_member comes pre-accepted * no notification on force accept * fixes tests * merge fixes
This commit is contained in:
@@ -109,8 +109,14 @@ pub async fn filter_authorized_projects(
|
||||
"
|
||||
SELECT m.id id, m.team_id team_id FROM team_members tm
|
||||
INNER JOIN mods m ON m.team_id = tm.team_id
|
||||
LEFT JOIN organizations o ON o.team_id = tm.team_id
|
||||
WHERE (tm.team_id = ANY($1) or o.id = ANY($2)) AND tm.user_id = $3
|
||||
WHERE tm.team_id = ANY($1) AND tm.user_id = $3
|
||||
|
||||
UNION
|
||||
|
||||
SELECT m.id id, m.team_id team_id FROM team_members tm
|
||||
INNER JOIN organizations o ON o.team_id = tm.team_id
|
||||
INNER JOIN mods m ON m.organization_id = o.id
|
||||
WHERE o.id = ANY($2) AND tm.user_id = $3
|
||||
",
|
||||
&check_projects
|
||||
.iter()
|
||||
@@ -126,7 +132,8 @@ pub async fn filter_authorized_projects(
|
||||
.try_for_each(|e| {
|
||||
if let Some(row) = e.right() {
|
||||
check_projects.retain(|x| {
|
||||
let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id;
|
||||
let bool =
|
||||
Some(x.inner.id.0) == row.id && Some(x.inner.team_id.0) == row.team_id;
|
||||
|
||||
if bool {
|
||||
return_projects.push(x.clone().into());
|
||||
@@ -160,15 +167,35 @@ pub async fn is_authorized_version(
|
||||
let user_id: models::ids::UserId = user.id.into();
|
||||
|
||||
let version_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 WHERE m.id = $1)",
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM mods m
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2
|
||||
WHERE m.id = $1
|
||||
)",
|
||||
version_data.project_id as database::models::ids::ProjectId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&***pool)
|
||||
.await?
|
||||
.exists;
|
||||
.fetch_one(&***pool)
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
authorized = version_exists.unwrap_or(false);
|
||||
let version_organization_exists = sqlx::query!(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM mods m
|
||||
INNER JOIN organizations o ON m.organization_id = o.id
|
||||
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2
|
||||
WHERE m.id = $1
|
||||
)",
|
||||
version_data.project_id as database::models::ids::ProjectId,
|
||||
user_id as database::models::ids::UserId,
|
||||
)
|
||||
.fetch_one(&***pool)
|
||||
.await?
|
||||
.exists;
|
||||
|
||||
authorized = version_exists
|
||||
.or(version_organization_exists)
|
||||
.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,9 +219,6 @@ pub struct ProjectTypeId(pub i32);
|
||||
pub struct StatusId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct SideTypeId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct GameId(pub i32);
|
||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[sqlx(transparent)]
|
||||
|
||||
@@ -339,12 +339,6 @@ pub struct QueryLoaderFieldEnumValue {
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct SideType {
|
||||
pub id: SideTypeId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl LoaderField {
|
||||
pub async fn get_field<'a, E>(
|
||||
field: &str,
|
||||
|
||||
@@ -256,31 +256,9 @@ impl Organization {
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<()>, super::DatabaseError> {
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let organization = Self::get_id(id, &mut **transaction, redis).await?;
|
||||
|
||||
if let Some(organization) = organization {
|
||||
let projects: Vec<ProjectId> = sqlx::query!(
|
||||
"
|
||||
SELECT m.id
|
||||
FROM mods m
|
||||
WHERE m.organization_id = $1
|
||||
",
|
||||
id as OrganizationId,
|
||||
)
|
||||
.fetch_many(&mut **transaction)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
|
||||
.try_collect::<Vec<ProjectId>>()
|
||||
.await?;
|
||||
|
||||
for project_id in projects {
|
||||
let _result =
|
||||
super::project_item::Project::remove(project_id, transaction, redis).await?;
|
||||
}
|
||||
|
||||
Organization::clear_cache(id, Some(organization.name), redis).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM organizations
|
||||
|
||||
@@ -412,10 +412,10 @@ impl TeamMember {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO team_members (
|
||||
id, team_id, user_id, role, permissions, organization_permissions, accepted
|
||||
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
)
|
||||
",
|
||||
self.id as TeamMemberId,
|
||||
@@ -424,6 +424,7 @@ impl TeamMember {
|
||||
self.role,
|
||||
self.permissions.bits() as i64,
|
||||
self.organization_permissions.map(|p| p.bits() as i64),
|
||||
self.is_owner,
|
||||
self.accepted,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
@@ -576,20 +577,28 @@ impl TeamMember {
|
||||
pub async fn get_from_user_id_project<'a, 'b, E>(
|
||||
id: ProjectId,
|
||||
user_id: UserId,
|
||||
allow_pending: bool,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let accepted = if allow_pending {
|
||||
vec![true, false]
|
||||
} else {
|
||||
vec![true]
|
||||
};
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
|
||||
FROM mods m
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE
|
||||
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3)
|
||||
WHERE m.id = $1
|
||||
",
|
||||
id as ProjectId,
|
||||
user_id as UserId
|
||||
user_id as UserId,
|
||||
&accepted
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
@@ -618,20 +627,27 @@ impl TeamMember {
|
||||
pub async fn get_from_user_id_organization<'a, 'b, E>(
|
||||
id: OrganizationId,
|
||||
user_id: UserId,
|
||||
allow_pending: bool,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let accepted = if allow_pending {
|
||||
vec![true, false]
|
||||
} else {
|
||||
vec![true]
|
||||
};
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
|
||||
FROM organizations o
|
||||
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE
|
||||
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3)
|
||||
WHERE o.id = $1
|
||||
",
|
||||
id as OrganizationId,
|
||||
user_id as UserId
|
||||
user_id as UserId,
|
||||
&accepted
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
@@ -27,6 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
// 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)
|
||||
// They can be differentiated by the "organization_permissions" field being null or not
|
||||
#[get("{id}/members")]
|
||||
pub async fn team_members_get_project(
|
||||
req: HttpRequest,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::database::models::{generate_organization_id, team_item, Organization}
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::ids::base62_impl::parse_base62;
|
||||
use crate::models::ids::UserId;
|
||||
use crate::models::organizations::OrganizationId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
@@ -17,6 +18,7 @@ use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use futures::TryStreamExt;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
@@ -65,7 +67,6 @@ pub async fn organization_projects_get(
|
||||
.ok();
|
||||
|
||||
let possible_organization_id: Option<u64> = parse_base62(&info).ok();
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
"
|
||||
@@ -503,6 +504,7 @@ pub async fn organization_delete(
|
||||
let team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
@@ -522,7 +524,55 @@ pub async fn organization_delete(
|
||||
}
|
||||
}
|
||||
|
||||
let owner_id = sqlx::query!(
|
||||
"
|
||||
SELECT user_id FROM team_members
|
||||
WHERE team_id = $1 AND is_owner = TRUE
|
||||
",
|
||||
organization.team_id as database::models::ids::TeamId
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await?
|
||||
.user_id;
|
||||
let owner_id = database::models::ids::UserId(owner_id);
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
// Handle projects- every project that is in this organization needs to have its owner changed the organization owner
|
||||
// Now, no project should have an owner if it is in an organization, and also
|
||||
// the owner of an organization should not be a team member in any project
|
||||
let organization_project_teams = sqlx::query!(
|
||||
"
|
||||
SELECT t.id FROM organizations o
|
||||
INNER JOIN mods m ON m.organization_id = o.id
|
||||
INNER JOIN teams t ON t.id = m.team_id
|
||||
WHERE o.id = $1 AND $1 IS NOT NULL
|
||||
",
|
||||
organization.id as database::models::ids::OrganizationId
|
||||
)
|
||||
.fetch_many(&mut *transaction)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| crate::database::models::TeamId(c.id))) })
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
for organization_project_team in organization_project_teams.iter() {
|
||||
let new_id =
|
||||
crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||
let member = TeamMember {
|
||||
id: new_id,
|
||||
team_id: *organization_project_team,
|
||||
user_id: owner_id,
|
||||
role: "Inherited Owner".to_string(),
|
||||
is_owner: true,
|
||||
permissions: ProjectPermissions::all(),
|
||||
organization_permissions: None,
|
||||
accepted: true,
|
||||
payouts_split: Decimal::ZERO,
|
||||
ordering: 0,
|
||||
};
|
||||
member.insert(&mut transaction).await?;
|
||||
}
|
||||
// Safely remove the organization
|
||||
let result =
|
||||
database::models::Organization::remove(organization.id, &mut transaction, &redis).await?;
|
||||
|
||||
@@ -531,6 +581,10 @@ pub async fn organization_delete(
|
||||
database::models::Organization::clear_cache(organization.id, Some(organization.name), &redis)
|
||||
.await?;
|
||||
|
||||
for team_id in organization_project_teams {
|
||||
database::models::TeamMember::clear_cache(team_id, &redis).await?;
|
||||
}
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
@@ -581,6 +635,7 @@ pub async fn organization_projects_add(
|
||||
let project_team_member = database::models::TeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
@@ -589,6 +644,7 @@ pub async fn organization_projects_add(
|
||||
let organization_team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
@@ -622,6 +678,47 @@ pub async fn organization_projects_add(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
// The former owner is no longer an owner (as it is now 'owned' by the organization, 'given' to them)
|
||||
// The former owner is still a member of the project, but not an owner
|
||||
// When later removed from the organization, the project will be owned by whoever is specified as the new owner there
|
||||
if !current_user.role.is_admin() {
|
||||
let team_member_id = project_team_member.id;
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE team_members
|
||||
SET is_owner = FALSE
|
||||
WHERE id = $1
|
||||
",
|
||||
team_member_id as database::models::ids::TeamMemberId
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// The owner of the organization, should be removed as a member of the project, if they are
|
||||
// (As it is an organization project now, and they should not have more specific permissions)
|
||||
let organization_owner_user_id = sqlx::query!(
|
||||
"
|
||||
SELECT u.id
|
||||
FROM team_members
|
||||
INNER JOIN users u ON u.id = team_members.user_id
|
||||
WHERE team_id = $1 AND is_owner = TRUE
|
||||
",
|
||||
organization.team_id as database::models::ids::TeamId
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
let organization_owner_user_id =
|
||||
database::models::ids::UserId(organization_owner_user_id.id);
|
||||
|
||||
// If the owner of the organization is a member of the project, remove them
|
||||
database::models::TeamMember::delete(
|
||||
project_item.inner.team_id,
|
||||
organization_owner_user_id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
database::models::TeamMember::clear_cache(project_item.inner.team_id, &redis).await?;
|
||||
@@ -640,10 +737,18 @@ pub async fn organization_projects_add(
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OrganizationProjectRemoval {
|
||||
// A new owner must be supplied for the project.
|
||||
// That user must be a member of the organization, but not necessarily a member of the project.
|
||||
pub new_owner: UserId,
|
||||
}
|
||||
|
||||
pub async fn organization_projects_remove(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String, String)>,
|
||||
pool: web::Data<PgPool>,
|
||||
data: web::Json<OrganizationProjectRemoval>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@@ -683,6 +788,7 @@ pub async fn organization_projects_remove(
|
||||
let organization_team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
current_user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
@@ -696,7 +802,72 @@ pub async fn organization_projects_remove(
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if permissions.contains(OrganizationPermissions::REMOVE_PROJECT) {
|
||||
// Now that permissions are confirmed, we confirm the veracity of the new user as an org member
|
||||
database::models::TeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
data.new_owner.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The specified user is not a member of this organization!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Then, we get the team member of the project and that user (if it exists)
|
||||
// We use the team member get directly
|
||||
let new_owner = database::models::TeamMember::get_from_user_id_project(
|
||||
project_item.inner.id,
|
||||
data.new_owner.into(),
|
||||
true,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
// If the user is not a member of the project, we add them
|
||||
let new_owner = match new_owner {
|
||||
Some(new_owner) => new_owner,
|
||||
None => {
|
||||
let new_id =
|
||||
crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||
let member = TeamMember {
|
||||
id: new_id,
|
||||
team_id: project_item.inner.team_id,
|
||||
user_id: data.new_owner.into(),
|
||||
role: "Inherited Owner".to_string(),
|
||||
is_owner: false,
|
||||
permissions: ProjectPermissions::all(),
|
||||
organization_permissions: None,
|
||||
accepted: true,
|
||||
payouts_split: Decimal::ZERO,
|
||||
ordering: 0,
|
||||
};
|
||||
member.insert(&mut transaction).await?;
|
||||
member
|
||||
}
|
||||
};
|
||||
|
||||
// Set the new owner to fit owner
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE team_members
|
||||
SET
|
||||
is_owner = TRUE,
|
||||
accepted = TRUE,
|
||||
permissions = $1,
|
||||
organization_permissions = NULL,
|
||||
role = 'Inherited Owner'
|
||||
WHERE (id = $1)
|
||||
",
|
||||
new_owner.id as database::models::ids::TeamMemberId
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
// 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)
|
||||
// They can be differentiated by the "organization_permissions" field being null or not
|
||||
pub async fn team_members_get_project(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
@@ -495,9 +496,39 @@ pub async fn add_team_member(
|
||||
));
|
||||
}
|
||||
}
|
||||
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_user =
|
||||
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 mut force_accepted = false;
|
||||
if let TeamAssociationId::Project(pid) = team_association {
|
||||
// We cannot add the owner to a project team in their own org
|
||||
let organization =
|
||||
Organization::get_associated_organization_project_id(pid, &**pool).await?;
|
||||
let new_user_organization_team_member = if let Some(organization) = &organization {
|
||||
TeamMember::get_from_user_id(organization.team_id, new_user.id, &**pool).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if new_user_organization_team_member
|
||||
.as_ref()
|
||||
.map(|tm| tm.is_owner)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot add the owner of an organization to a project team owned by that organization".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// In the case of adding a user that is in an org, to a project that is owned by that same org,
|
||||
// the user is automatically accepted into that project.
|
||||
// That is because the user is part of the org, and project teame-membership in an org can also be used to reduce permissions
|
||||
// (Which should not be a deniable action by that user)
|
||||
if new_user_organization_team_member.is_some() {
|
||||
force_accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||
TeamMember {
|
||||
@@ -508,37 +539,40 @@ pub async fn add_team_member(
|
||||
is_owner: false, // Cannot just create an owner
|
||||
permissions: new_member.permissions,
|
||||
organization_permissions: new_member.organization_permissions,
|
||||
accepted: false,
|
||||
accepted: force_accepted,
|
||||
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(),
|
||||
},
|
||||
// If the user has an opportunity to accept the invite, send a notification
|
||||
if !force_accepted {
|
||||
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?;
|
||||
}
|
||||
.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(),
|
||||
},
|
||||
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?;
|
||||
}
|
||||
.insert(new_member.user_id.into(), &mut transaction, &redis)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +627,9 @@ pub async fn edit_team_member(
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if edit_member_db.is_owner && edit_member.permissions.is_some() {
|
||||
if edit_member_db.is_owner
|
||||
&& (edit_member.permissions.is_some() || edit_member.organization_permissions.is_some())
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The owner's permission's in a team cannot be edited".to_string(),
|
||||
));
|
||||
@@ -723,8 +759,9 @@ pub async fn transfer_ownership(
|
||||
|
||||
// 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 {
|
||||
// There shouldnt be an ownr on these projects in these cases, but just in case.
|
||||
let team_association_id = Team::get_association(id.into(), &**pool).await?;
|
||||
if let Some(TeamAssociationId::Project(pid)) = team_association_id {
|
||||
let result = Project::get_id(pid, &**pool, &redis).await?;
|
||||
if let Some(project_item) = result {
|
||||
if project_item.inner.organization_id.is_some() {
|
||||
@@ -785,7 +822,14 @@ pub async fn transfer_ownership(
|
||||
id.into(),
|
||||
new_owner.user_id.into(),
|
||||
Some(ProjectPermissions::all()),
|
||||
Some(OrganizationPermissions::all()),
|
||||
if matches!(
|
||||
team_association_id,
|
||||
Some(TeamAssociationId::Organization(_))
|
||||
) {
|
||||
Some(OrganizationPermissions::all())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -795,8 +839,44 @@ pub async fn transfer_ownership(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_teams_edited =
|
||||
if let Some(TeamAssociationId::Organization(oid)) = team_association_id {
|
||||
// The owner of ALL projects that this organization owns, if applicable, should be removed as members of the project,
|
||||
// if they are members of those projects.
|
||||
// (As they are the org owners for them, and they should not have more specific permissions)
|
||||
|
||||
// First, get team id for every project owned by this organization
|
||||
let team_ids = sqlx::query!(
|
||||
"
|
||||
SELECT m.team_id FROM organizations o
|
||||
INNER JOIN mods m ON m.organization_id = o.id
|
||||
WHERE o.id = $1 AND $1 IS NOT NULL
|
||||
",
|
||||
oid.0 as i64
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let team_ids: Vec<crate::database::models::ids::TeamId> = team_ids
|
||||
.into_iter()
|
||||
.map(|x| TeamId(x.team_id as u64).into())
|
||||
.collect();
|
||||
|
||||
// If the owner of the organization is a member of the project, remove them
|
||||
for team_id in team_ids.iter() {
|
||||
TeamMember::delete(*team_id, new_owner.user_id.into(), &mut transaction).await?;
|
||||
}
|
||||
|
||||
team_ids
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
TeamMember::clear_cache(id.into(), &redis).await?;
|
||||
for team_id in project_teams_edited {
|
||||
TeamMember::clear_cache(team_id, &redis).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ async fn version_create_inner(
|
||||
let team_member = models::TeamMember::get_from_user_id_project(
|
||||
project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?;
|
||||
@@ -609,6 +610,7 @@ async fn upload_file_to_version_inner(
|
||||
let team_member = models::TeamMember::get_from_user_id_project(
|
||||
version.inner.project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&mut **transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -586,6 +586,7 @@ pub async fn delete_file(
|
||||
database::models::TeamMember::get_from_user_id_organization(
|
||||
organization.id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -273,6 +273,7 @@ pub async fn version_edit_helper(
|
||||
let team_member = database::models::TeamMember::get_from_user_id_project(
|
||||
version_item.inner.project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
@@ -855,6 +856,7 @@ pub async fn version_delete(
|
||||
let team_member = database::models::TeamMember::get_from_user_id_project(
|
||||
version.inner.project_id,
|
||||
user.id.into(),
|
||||
false,
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user