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:
23
.sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json
generated
Normal file
23
.sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT EXISTS(\n SELECT 1 FROM mods m \n INNER JOIN organizations o ON m.organization_id = o.id\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 \n WHERE m.id = $1\n )",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "exists",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN mods m ON m.team_id = tm.team_id\n LEFT JOIN organizations o ON o.team_id = tm.team_id\n WHERE (tm.team_id = ANY($1) or o.id = ANY($2)) AND tm.user_id = $3\n ",
|
"query": "\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN mods m ON m.team_id = tm.team_id\n WHERE tm.team_id = ANY($1) AND tm.user_id = $3\n\n UNION\n\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN organizations o ON o.team_id = tm.team_id\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE o.id = ANY($2) AND tm.user_id = $3\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
null,
|
||||||
false
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2"
|
"hash": "23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b"
|
||||||
}
|
}
|
||||||
22
.sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json
generated
Normal file
22
.sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT m.team_id FROM organizations o\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE o.id = $1 AND $1 IS NOT NULL\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "team_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74"
|
||||||
|
}
|
||||||
22
.sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json
generated
Normal file
22
.sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT u.id \n FROM team_members\n INNER JOIN users u ON u.id = team_members.user_id\n WHERE team_id = $1 AND is_owner = TRUE\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n 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\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE\n WHERE o.id = $1\n ",
|
"query": "\n 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\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3)\n WHERE o.id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8",
|
"Int8",
|
||||||
"Int8"
|
"Int8",
|
||||||
|
"BoolArray"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
@@ -73,5 +74,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e"
|
"hash": "389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT m.id\n FROM mods m\n WHERE m.organization_id = $1\n ",
|
"query": "\n SELECT user_id FROM team_members\n WHERE team_id = $1 AND is_owner = TRUE\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "id",
|
"name": "user_id",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -18,5 +18,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "957d0b3f6ad7d20f54548b05e82935cd18adc723f819fd071d8c97ec3885381a"
|
"hash": "7c88d4f3a4342676773ae0c90ec17703fe59b218c851aaee02ba89f30385a315"
|
||||||
}
|
}
|
||||||
23
.sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json
generated
Normal file
23
.sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT EXISTS(\n SELECT 1 FROM mods m \n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 \n WHERE m.id = $1\n )",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "exists",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ",
|
"query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -11,10 +11,11 @@
|
|||||||
"Varchar",
|
"Varchar",
|
||||||
"Int8",
|
"Int8",
|
||||||
"Int8",
|
"Int8",
|
||||||
|
"Bool",
|
||||||
"Bool"
|
"Bool"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20"
|
"hash": "8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645"
|
||||||
}
|
}
|
||||||
22
.sqlx/query-d3c5adda017df70a88983baa82e3feb0a3eb432ed2b9d3be0e7a0bc6b2421cdd.json
generated
Normal file
22
.sqlx/query-d3c5adda017df70a88983baa82e3feb0a3eb432ed2b9d3be0e7a0bc6b2421cdd.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT t.id FROM organizations o\n INNER JOIN mods m ON m.organization_id = o.id\n INNER JOIN teams t ON t.id = m.team_id\n WHERE o.id = $1 AND $1 IS NOT NULL\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d3c5adda017df70a88983baa82e3feb0a3eb432ed2b9d3be0e7a0bc6b2421cdd"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n 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\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ",
|
"query": "\n 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\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3)\n WHERE m.id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8",
|
"Int8",
|
||||||
"Int8"
|
"Int8",
|
||||||
|
"BoolArray"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
@@ -73,5 +74,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895"
|
"hash": "ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d"
|
||||||
}
|
}
|
||||||
14
.sqlx/query-ee6b35a83723e0b753bd30819bad9bb356455b6c0b87cdf1b43fa576ccc5bef5.json
generated
Normal file
14
.sqlx/query-ee6b35a83723e0b753bd30819bad9bb356455b6c0b87cdf1b43fa576ccc5bef5.json
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE team_members\n SET is_owner = FALSE\n WHERE id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ee6b35a83723e0b753bd30819bad9bb356455b6c0b87cdf1b43fa576ccc5bef5"
|
||||||
|
}
|
||||||
14
.sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json
generated
Normal file
14
.sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE team_members\n SET \n is_owner = TRUE,\n accepted = TRUE,\n permissions = $1,\n organization_permissions = NULL,\n role = 'Inherited Owner'\n WHERE (id = $1)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0"
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"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)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "exists",
|
|
||||||
"type_info": "Bool"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8",
|
|
||||||
"Int8"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
null
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9"
|
|
||||||
}
|
|
||||||
10
migrations/20231213103100_enforces-owner-unique.sql
Normal file
10
migrations/20231213103100_enforces-owner-unique.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Enforces that there can only be one owner per team
|
||||||
|
CREATE UNIQUE INDEX idx_one_owner_per_team
|
||||||
|
ON team_members (team_id)
|
||||||
|
WHERE is_owner = TRUE;
|
||||||
|
|
||||||
|
-- Enforces one team_member per user/team
|
||||||
|
CREATE UNIQUE INDEX idx_unique_user_team
|
||||||
|
ON team_members (user_id, team_id);
|
||||||
|
|
||||||
|
|
||||||
@@ -109,8 +109,14 @@ pub async fn filter_authorized_projects(
|
|||||||
"
|
"
|
||||||
SELECT m.id id, m.team_id team_id FROM team_members tm
|
SELECT m.id id, m.team_id team_id FROM team_members tm
|
||||||
INNER JOIN mods m ON m.team_id = tm.team_id
|
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) AND tm.user_id = $3
|
||||||
WHERE (tm.team_id = ANY($1) or o.id = ANY($2)) 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
|
&check_projects
|
||||||
.iter()
|
.iter()
|
||||||
@@ -126,7 +132,8 @@ pub async fn filter_authorized_projects(
|
|||||||
.try_for_each(|e| {
|
.try_for_each(|e| {
|
||||||
if let Some(row) = e.right() {
|
if let Some(row) = e.right() {
|
||||||
check_projects.retain(|x| {
|
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 {
|
if bool {
|
||||||
return_projects.push(x.clone().into());
|
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 user_id: models::ids::UserId = user.id.into();
|
||||||
|
|
||||||
let version_exists = sqlx::query!(
|
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,
|
version_data.project_id as database::models::ids::ProjectId,
|
||||||
user_id as database::models::ids::UserId,
|
user_id as database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.fetch_one(&***pool)
|
.fetch_one(&***pool)
|
||||||
.await?
|
.await?
|
||||||
.exists;
|
.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);
|
pub struct StatusId(pub i32);
|
||||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
pub struct SideTypeId(pub i32);
|
|
||||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
|
|
||||||
#[sqlx(transparent)]
|
|
||||||
pub struct GameId(pub i32);
|
pub struct GameId(pub i32);
|
||||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[sqlx(transparent)]
|
#[sqlx(transparent)]
|
||||||
|
|||||||
@@ -339,12 +339,6 @@ pub struct QueryLoaderFieldEnumValue {
|
|||||||
pub metadata: Option<serde_json::Value>,
|
pub metadata: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct SideType {
|
|
||||||
pub id: SideTypeId,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoaderField {
|
impl LoaderField {
|
||||||
pub async fn get_field<'a, E>(
|
pub async fn get_field<'a, E>(
|
||||||
field: &str,
|
field: &str,
|
||||||
|
|||||||
@@ -256,31 +256,9 @@ impl Organization {
|
|||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<Option<()>, super::DatabaseError> {
|
) -> Result<Option<()>, super::DatabaseError> {
|
||||||
use futures::TryStreamExt;
|
|
||||||
|
|
||||||
let organization = Self::get_id(id, &mut **transaction, redis).await?;
|
let organization = Self::get_id(id, &mut **transaction, redis).await?;
|
||||||
|
|
||||||
if let Some(organization) = organization {
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
DELETE FROM organizations
|
DELETE FROM organizations
|
||||||
|
|||||||
@@ -412,10 +412,10 @@ impl TeamMember {
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO team_members (
|
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 (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as TeamMemberId,
|
self.id as TeamMemberId,
|
||||||
@@ -424,6 +424,7 @@ impl TeamMember {
|
|||||||
self.role,
|
self.role,
|
||||||
self.permissions.bits() as i64,
|
self.permissions.bits() as i64,
|
||||||
self.organization_permissions.map(|p| p.bits() as i64),
|
self.organization_permissions.map(|p| p.bits() as i64),
|
||||||
|
self.is_owner,
|
||||||
self.accepted,
|
self.accepted,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
@@ -576,20 +577,28 @@ impl TeamMember {
|
|||||||
pub async fn get_from_user_id_project<'a, 'b, E>(
|
pub async fn get_from_user_id_project<'a, 'b, E>(
|
||||||
id: ProjectId,
|
id: ProjectId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
allow_pending: bool,
|
||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<Self>, super::DatabaseError>
|
) -> Result<Option<Self>, super::DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
let accepted = if allow_pending {
|
||||||
|
vec![true, false]
|
||||||
|
} else {
|
||||||
|
vec![true]
|
||||||
|
};
|
||||||
|
|
||||||
let result = sqlx::query!(
|
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
|
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
|
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
|
WHERE m.id = $1
|
||||||
",
|
",
|
||||||
id as ProjectId,
|
id as ProjectId,
|
||||||
user_id as UserId
|
user_id as UserId,
|
||||||
|
&accepted
|
||||||
)
|
)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -618,20 +627,27 @@ impl TeamMember {
|
|||||||
pub async fn get_from_user_id_organization<'a, 'b, E>(
|
pub async fn get_from_user_id_organization<'a, 'b, E>(
|
||||||
id: OrganizationId,
|
id: OrganizationId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
allow_pending: bool,
|
||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<Self>, super::DatabaseError>
|
) -> Result<Option<Self>, super::DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
let accepted = if allow_pending {
|
||||||
|
vec![true, false]
|
||||||
|
} else {
|
||||||
|
vec![true]
|
||||||
|
};
|
||||||
let result = sqlx::query!(
|
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
|
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
|
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
|
WHERE o.id = $1
|
||||||
",
|
",
|
||||||
id as OrganizationId,
|
id as OrganizationId,
|
||||||
user_id as UserId
|
user_id as UserId,
|
||||||
|
&accepted
|
||||||
)
|
)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
// including the team members of the project's team, but
|
// 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
|
// 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)
|
// (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")]
|
#[get("{id}/members")]
|
||||||
pub async fn team_members_get_project(
|
pub async fn team_members_get_project(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::database::models::{generate_organization_id, team_item, Organization}
|
|||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models::ids::base62_impl::parse_base62;
|
use crate::models::ids::base62_impl::parse_base62;
|
||||||
|
use crate::models::ids::UserId;
|
||||||
use crate::models::organizations::OrganizationId;
|
use crate::models::organizations::OrganizationId;
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
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::util::validate::validation_errors_to_string;
|
||||||
use crate::{database, models};
|
use crate::{database, models};
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use futures::TryStreamExt;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -65,7 +67,6 @@ pub async fn organization_projects_get(
|
|||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let possible_organization_id: Option<u64> = parse_base62(&info).ok();
|
let possible_organization_id: Option<u64> = parse_base62(&info).ok();
|
||||||
use futures::TryStreamExt;
|
|
||||||
|
|
||||||
let project_ids = sqlx::query!(
|
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(
|
let team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||||
organization.id,
|
organization.id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await
|
.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?;
|
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 =
|
let result =
|
||||||
database::models::Organization::remove(organization.id, &mut transaction, &redis).await?;
|
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)
|
database::models::Organization::clear_cache(organization.id, Some(organization.name), &redis)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
for team_id in organization_project_teams {
|
||||||
|
database::models::TeamMember::clear_cache(team_id, &redis).await?;
|
||||||
|
}
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
} else {
|
} else {
|
||||||
@@ -581,6 +635,7 @@ pub async fn organization_projects_add(
|
|||||||
let project_team_member = database::models::TeamMember::get_from_user_id_project(
|
let project_team_member = database::models::TeamMember::get_from_user_id_project(
|
||||||
project_item.inner.id,
|
project_item.inner.id,
|
||||||
current_user.id.into(),
|
current_user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -589,6 +644,7 @@ pub async fn organization_projects_add(
|
|||||||
let organization_team_member = database::models::TeamMember::get_from_user_id_organization(
|
let organization_team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||||
organization.id,
|
organization.id,
|
||||||
current_user.id.into(),
|
current_user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -622,6 +678,47 @@ pub async fn organization_projects_add(
|
|||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.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?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
database::models::TeamMember::clear_cache(project_item.inner.team_id, &redis).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())
|
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(
|
pub async fn organization_projects_remove(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(String, String)>,
|
info: web::Path<(String, String)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
|
data: web::Json<OrganizationProjectRemoval>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> 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(
|
let organization_team_member = database::models::TeamMember::get_from_user_id_organization(
|
||||||
organization.id,
|
organization.id,
|
||||||
current_user.id.into(),
|
current_user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -696,7 +802,72 @@ pub async fn organization_projects_remove(
|
|||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if permissions.contains(OrganizationPermissions::REMOVE_PROJECT) {
|
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?;
|
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!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
// including the team members of the project's team, but
|
// 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
|
// 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)
|
// (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(
|
pub async fn team_members_get_project(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(String,)>,
|
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)
|
let new_user =
|
||||||
.await?
|
crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis)
|
||||||
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
|
.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?;
|
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
|
||||||
TeamMember {
|
TeamMember {
|
||||||
@@ -508,37 +539,40 @@ pub async fn add_team_member(
|
|||||||
is_owner: false, // Cannot just create an owner
|
is_owner: false, // Cannot just create an owner
|
||||||
permissions: new_member.permissions,
|
permissions: new_member.permissions,
|
||||||
organization_permissions: new_member.organization_permissions,
|
organization_permissions: new_member.organization_permissions,
|
||||||
accepted: false,
|
accepted: force_accepted,
|
||||||
payouts_split: new_member.payouts_split,
|
payouts_split: new_member.payouts_split,
|
||||||
ordering: new_member.ordering,
|
ordering: new_member.ordering,
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match team_association {
|
// If the user has an opportunity to accept the invite, send a notification
|
||||||
TeamAssociationId::Project(pid) => {
|
if !force_accepted {
|
||||||
NotificationBuilder {
|
match team_association {
|
||||||
body: NotificationBody::TeamInvite {
|
TeamAssociationId::Project(pid) => {
|
||||||
project_id: pid.into(),
|
NotificationBuilder {
|
||||||
team_id: team_id.into(),
|
body: NotificationBody::TeamInvite {
|
||||||
invited_by: current_user.id,
|
project_id: pid.into(),
|
||||||
role: new_member.role.clone(),
|
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)
|
TeamAssociationId::Organization(oid) => {
|
||||||
.await?;
|
NotificationBuilder {
|
||||||
}
|
body: NotificationBody::OrganizationInvite {
|
||||||
TeamAssociationId::Organization(oid) => {
|
organization_id: oid.into(),
|
||||||
NotificationBuilder {
|
team_id: team_id.into(),
|
||||||
body: NotificationBody::OrganizationInvite {
|
invited_by: current_user.id,
|
||||||
organization_id: oid.into(),
|
role: new_member.role.clone(),
|
||||||
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?;
|
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(
|
return Err(ApiError::InvalidInput(
|
||||||
"The owner's permission's in a team cannot be edited".to_string(),
|
"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
|
// 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
|
// These are owned by the organization owner, and must be removed from the organization first
|
||||||
let pid = Team::get_association(id.into(), &**pool).await?;
|
// There shouldnt be an ownr on these projects in these cases, but just in case.
|
||||||
if let Some(TeamAssociationId::Project(pid)) = pid {
|
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?;
|
let result = Project::get_id(pid, &**pool, &redis).await?;
|
||||||
if let Some(project_item) = result {
|
if let Some(project_item) = result {
|
||||||
if project_item.inner.organization_id.is_some() {
|
if project_item.inner.organization_id.is_some() {
|
||||||
@@ -785,7 +822,14 @@ pub async fn transfer_ownership(
|
|||||||
id.into(),
|
id.into(),
|
||||||
new_owner.user_id.into(),
|
new_owner.user_id.into(),
|
||||||
Some(ProjectPermissions::all()),
|
Some(ProjectPermissions::all()),
|
||||||
Some(OrganizationPermissions::all()),
|
if matches!(
|
||||||
|
team_association_id,
|
||||||
|
Some(TeamAssociationId::Organization(_))
|
||||||
|
) {
|
||||||
|
Some(OrganizationPermissions::all())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -795,8 +839,44 @@ pub async fn transfer_ownership(
|
|||||||
)
|
)
|
||||||
.await?;
|
.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?;
|
transaction.commit().await?;
|
||||||
TeamMember::clear_cache(id.into(), &redis).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(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ async fn version_create_inner(
|
|||||||
let team_member = models::TeamMember::get_from_user_id_project(
|
let team_member = models::TeamMember::get_from_user_id_project(
|
||||||
project_id,
|
project_id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&mut **transaction,
|
&mut **transaction,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -609,6 +610,7 @@ async fn upload_file_to_version_inner(
|
|||||||
let team_member = models::TeamMember::get_from_user_id_project(
|
let team_member = models::TeamMember::get_from_user_id_project(
|
||||||
version.inner.project_id,
|
version.inner.project_id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&mut **transaction,
|
&mut **transaction,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -586,6 +586,7 @@ pub async fn delete_file(
|
|||||||
database::models::TeamMember::get_from_user_id_organization(
|
database::models::TeamMember::get_from_user_id_organization(
|
||||||
organization.id,
|
organization.id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ pub async fn version_edit_helper(
|
|||||||
let team_member = database::models::TeamMember::get_from_user_id_project(
|
let team_member = database::models::TeamMember::get_from_user_id_project(
|
||||||
version_item.inner.project_id,
|
version_item.inner.project_id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -855,6 +856,7 @@ pub async fn version_delete(
|
|||||||
let team_member = database::models::TeamMember::get_from_user_id_project(
|
let team_member = database::models::TeamMember::get_from_user_id_project(
|
||||||
version.inner.project_id,
|
version.inner.project_id,
|
||||||
user.id.into(),
|
user.id.into(),
|
||||||
|
false,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use actix_web::{
|
|||||||
test::{self, TestRequest},
|
test::{self, TestRequest},
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use labrinth::models::{organizations::Organization, v3::projects::Project};
|
use labrinth::models::{organizations::Organization, users::UserId, v3::projects::Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::common::api_common::{request_data::ImageData, Api, AppendsOptionalPat};
|
use crate::common::api_common::{request_data::ImageData, Api, AppendsOptionalPat};
|
||||||
@@ -162,12 +162,16 @@ impl ApiV3 {
|
|||||||
&self,
|
&self,
|
||||||
id_or_title: &str,
|
id_or_title: &str,
|
||||||
project_id_or_slug: &str,
|
project_id_or_slug: &str,
|
||||||
|
new_owner_user_id: UserId,
|
||||||
pat: Option<&str>,
|
pat: Option<&str>,
|
||||||
) -> ServiceResponse {
|
) -> ServiceResponse {
|
||||||
let req = test::TestRequest::delete()
|
let req = test::TestRequest::delete()
|
||||||
.uri(&format!(
|
.uri(&format!(
|
||||||
"/v3/organization/{id_or_title}/projects/{project_id_or_slug}"
|
"/v3/organization/{id_or_title}/projects/{project_id_or_slug}"
|
||||||
))
|
))
|
||||||
|
.set_json(json!({
|
||||||
|
"new_owner": new_owner_user_id,
|
||||||
|
}))
|
||||||
.append_pat(pat)
|
.append_pat(pat)
|
||||||
.to_request();
|
.to_request();
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ impl ApiV3 {
|
|||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
test::read_body_json(resp).await
|
test::read_body_json(resp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_project_members_deserialized(
|
||||||
|
&self,
|
||||||
|
project_id: &str,
|
||||||
|
pat: Option<&str>,
|
||||||
|
) -> Vec<TeamMember> {
|
||||||
|
let resp = self.get_project_members(project_id, pat).await;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
test::read_body_json(resp).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
|
|||||||
@@ -1051,8 +1051,10 @@ async fn add_user_to_team(
|
|||||||
assert!(resp.status().is_success());
|
assert!(resp.status().is_success());
|
||||||
|
|
||||||
// Accept invitation
|
// Accept invitation
|
||||||
let resp = setup_api.join_team(team_id, user_pat).await;
|
setup_api.join_team(team_id, user_pat).await;
|
||||||
assert!(resp.status().is_success());
|
// This does not check if the join request was successful,
|
||||||
|
// as the join is not always needed- an org project + in-org invite
|
||||||
|
// will automatically go through.
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn modify_user_team_permissions(
|
async fn modify_user_team_permissions(
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use crate::common::{
|
use crate::common::{
|
||||||
api_common::ApiTeams,
|
api_common::{ApiProject, ApiTeams},
|
||||||
database::{generate_random_name, ADMIN_USER_PAT, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID},
|
database::{
|
||||||
dummy_data::DummyImage,
|
generate_random_name, ADMIN_USER_PAT, ENEMY_USER_ID_PARSED, ENEMY_USER_PAT,
|
||||||
|
FRIEND_USER_ID_PARSED, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID, USER_USER_ID_PARSED,
|
||||||
|
},
|
||||||
|
dummy_data::{DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta},
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
api_v3::ApiV3,
|
api_v3::ApiV3,
|
||||||
@@ -9,7 +12,10 @@ use common::{
|
|||||||
environment::{with_test_environment, with_test_environment_all, TestEnvironment},
|
environment::{with_test_environment, with_test_environment_all, TestEnvironment},
|
||||||
permissions::{PermissionsTest, PermissionsTestContext},
|
permissions::{PermissionsTest, PermissionsTestContext},
|
||||||
};
|
};
|
||||||
use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions};
|
use labrinth::models::{
|
||||||
|
teams::{OrganizationPermissions, ProjectPermissions},
|
||||||
|
users::UserId,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
@@ -249,7 +255,12 @@ async fn add_remove_organization_projects() {
|
|||||||
// Remove project from organization
|
// Remove project from organization
|
||||||
let resp = test_env
|
let resp = test_env
|
||||||
.api
|
.api
|
||||||
.organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT)
|
.organization_remove_project(
|
||||||
|
zeta_organization_id,
|
||||||
|
alpha,
|
||||||
|
UserId(USER_USER_ID_PARSED as u64),
|
||||||
|
USER_USER_PAT,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
@@ -264,6 +275,357 @@ async fn add_remove_organization_projects() {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Like above, but specifically regarding ownership transferring
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn add_remove_organization_project_ownership_to_user() {
|
||||||
|
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||||
|
let DummyProjectAlpha {
|
||||||
|
project_id: alpha_project_id,
|
||||||
|
team_id: alpha_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.project_alpha;
|
||||||
|
let DummyProjectBeta {
|
||||||
|
project_id: beta_project_id,
|
||||||
|
team_id: beta_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.project_beta;
|
||||||
|
let DummyOrganizationZeta {
|
||||||
|
organization_id: zeta_organization_id,
|
||||||
|
team_id: zeta_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.organization_zeta;
|
||||||
|
|
||||||
|
// Add friend to alpha, beta, and zeta
|
||||||
|
for (team, organization) in [
|
||||||
|
(alpha_team_id, false),
|
||||||
|
(beta_team_id, false),
|
||||||
|
(zeta_team_id, true),
|
||||||
|
] {
|
||||||
|
let org_permissions = if organization {
|
||||||
|
Some(OrganizationPermissions::all())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.add_user_to_team(
|
||||||
|
team,
|
||||||
|
FRIEND_USER_ID,
|
||||||
|
Some(ProjectPermissions::all()),
|
||||||
|
org_permissions,
|
||||||
|
USER_USER_PAT,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Accept invites
|
||||||
|
let resp = test_env.api.join_team(team, FRIEND_USER_PAT).await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each team, confirm there are two members, but only one owner of the project, and it is USER_USER_ID
|
||||||
|
for team in [alpha_team_id, beta_team_id, zeta_team_id] {
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(team, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(members.len(), 2);
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer ownership of beta project to FRIEND
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.transfer_team_ownership(beta_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Confirm there are still two users, but now FRIEND_USER_ID is the owner
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(beta_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(members.len(), 2);
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
|
||||||
|
|
||||||
|
// Add alpha, beta to zeta organization
|
||||||
|
for (project_id, pat) in [
|
||||||
|
(alpha_project_id, USER_USER_PAT),
|
||||||
|
(beta_project_id, FRIEND_USER_PAT),
|
||||||
|
] {
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.organization_add_project(zeta_organization_id, project_id, pat)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
|
// Get and confirm it has been added
|
||||||
|
let project = test_env.api.get_project_deserialized(project_id, pat).await;
|
||||||
|
assert_eq!(
|
||||||
|
&project.organization.unwrap().to_string(),
|
||||||
|
zeta_organization_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both alpha and beta project should have:
|
||||||
|
// - 1 member, FRIEND_USER_ID
|
||||||
|
// - No owner.
|
||||||
|
// -> For alpha, user was removed as owner when it was added to the organization
|
||||||
|
// -> For beta, user was removed as owner when ownership was transferred to friend
|
||||||
|
// then friend was removed as owner when it was added to the organization
|
||||||
|
// -> In both cases, user was removed entirely as a team_member as it is now the owner of the organization
|
||||||
|
for team_id in [alpha_team_id, beta_team_id] {
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(members.len(), 1);
|
||||||
|
assert_eq!(members[0].user.id.to_string(), FRIEND_USER_ID);
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer ownership of zeta organization to FRIEND
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Confirm there are no members of the alpha project OR the beta project
|
||||||
|
// - Friend was removed as a member of these projects when ownership was transferred to them
|
||||||
|
for team_id in [alpha_team_id, beta_team_id] {
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert!(members.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// As user, cannot add friend to alpha project, as they are the org owner
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 400);
|
||||||
|
|
||||||
|
// As friend, can add user to alpha project, as they are not the org owner
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.add_user_to_team(alpha_team_id, USER_USER_ID, None, None, FRIEND_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// At this point, friend owns the org
|
||||||
|
// Alpha member has user as a member, but not as an owner
|
||||||
|
// Neither project has an owner, as they are owned by the org
|
||||||
|
|
||||||
|
// Remove project from organization with a user that is not an organization member
|
||||||
|
// This should fail as we cannot give a project to a user that is not a member of the organization
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.organization_remove_project(
|
||||||
|
zeta_organization_id,
|
||||||
|
alpha_project_id,
|
||||||
|
UserId(ENEMY_USER_ID_PARSED as u64),
|
||||||
|
USER_USER_PAT,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 400);
|
||||||
|
|
||||||
|
// Remove project from organization with a user that is an organization member, and a project member
|
||||||
|
// This should succeed
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.organization_remove_project(
|
||||||
|
zeta_organization_id,
|
||||||
|
alpha_project_id,
|
||||||
|
UserId(USER_USER_ID_PARSED as u64),
|
||||||
|
USER_USER_PAT,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
|
// Remove project from organization with a user that is an organization member, but not a project member
|
||||||
|
// This should succeed
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.organization_remove_project(
|
||||||
|
zeta_organization_id,
|
||||||
|
beta_project_id,
|
||||||
|
UserId(USER_USER_ID_PARSED as u64),
|
||||||
|
USER_USER_PAT,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
|
// For each of alpha and beta, confirm:
|
||||||
|
// - There is one member of each project, the owner, USER_USER_ID
|
||||||
|
// - They no longer have an attached organization
|
||||||
|
for team_id in [alpha_team_id, beta_team_id] {
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(members.len(), 1);
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
for project_id in [alpha_project_id, beta_project_id] {
|
||||||
|
let project = test_env
|
||||||
|
.api
|
||||||
|
.get_project_deserialized(project_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert!(project.organization.is_none());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn delete_organization_means_all_projects_to_org_owner() {
|
||||||
|
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||||
|
let DummyProjectAlpha {
|
||||||
|
project_id: alpha_project_id,
|
||||||
|
team_id: alpha_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.project_alpha;
|
||||||
|
let DummyProjectBeta {
|
||||||
|
project_id: beta_project_id,
|
||||||
|
team_id: beta_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.project_beta;
|
||||||
|
let DummyOrganizationZeta {
|
||||||
|
organization_id: zeta_organization_id,
|
||||||
|
team_id: zeta_team_id,
|
||||||
|
..
|
||||||
|
} = &test_env.dummy.organization_zeta;
|
||||||
|
|
||||||
|
// Create random project from enemy, ensure it wont get affected
|
||||||
|
let (enemy_project, _) = test_env
|
||||||
|
.api
|
||||||
|
.add_public_project("enemy_project", None, None, ENEMY_USER_PAT)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Add FRIEND
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Accept invite
|
||||||
|
let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Confirm there is only one owner of the project, and it is USER_USER_ID
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
|
||||||
|
|
||||||
|
// Add alpha to zeta organization
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
|
// Add beta to zeta organization
|
||||||
|
test_env
|
||||||
|
.api
|
||||||
|
.organization_add_project(zeta_organization_id, beta_project_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Add friend as a member of the beta project
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.add_user_to_team(beta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Try to accept invite
|
||||||
|
// This returns a failure, because since beta and FRIEND are in the organizations,
|
||||||
|
// they can be added to the project without an invite
|
||||||
|
let resp = test_env.api.join_team(beta_team_id, FRIEND_USER_PAT).await;
|
||||||
|
assert_eq!(resp.status(), 400);
|
||||||
|
|
||||||
|
// Confirm there is NO owner of the project, as it is owned by the organization
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 0);
|
||||||
|
|
||||||
|
// Transfer ownership of zeta organization to FRIEND
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Confirm there is NO owner of the project, as it is owned by the organization
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 0);
|
||||||
|
|
||||||
|
// Delete organization
|
||||||
|
let resp = test_env
|
||||||
|
.api
|
||||||
|
.delete_organization(zeta_organization_id, FRIEND_USER_PAT)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), 204);
|
||||||
|
|
||||||
|
// Confirm there is only one owner of the alpha project, and it is now FRIEND_USER_ID
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
|
||||||
|
|
||||||
|
// Confirm there is only one owner of the beta project, and it is now FRIEND_USER_ID
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(beta_team_id, USER_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
|
||||||
|
|
||||||
|
// Confirm there is only one member of the enemy project, and it is STILL ENEMY_USER_ID
|
||||||
|
let enemy_project = test_env
|
||||||
|
.api
|
||||||
|
.get_project_deserialized(&enemy_project.id.to_string(), ENEMY_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let members = test_env
|
||||||
|
.api
|
||||||
|
.get_team_members_deserialized(&enemy_project.team_id.to_string(), ENEMY_USER_PAT)
|
||||||
|
.await;
|
||||||
|
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
|
||||||
|
assert_eq!(user_member.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
user_member[0].user.id.to_string(),
|
||||||
|
ENEMY_USER_ID_PARSED.to_string()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn permissions_patch_organization() {
|
async fn permissions_patch_organization() {
|
||||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||||
@@ -480,6 +842,7 @@ async fn permissions_add_remove_project() {
|
|||||||
api.organization_remove_project(
|
api.organization_remove_project(
|
||||||
&ctx.organization_id.unwrap(),
|
&ctx.organization_id.unwrap(),
|
||||||
alpha_project_id,
|
alpha_project_id,
|
||||||
|
UserId(FRIEND_USER_ID_PARSED as u64),
|
||||||
ctx.test_pat.as_deref(),
|
ctx.test_pat.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use common::{database::*, scopes::ScopeTest};
|
|||||||
use labrinth::models::ids::base62_impl::parse_base62;
|
use labrinth::models::ids::base62_impl::parse_base62;
|
||||||
use labrinth::models::pats::Scopes;
|
use labrinth::models::pats::Scopes;
|
||||||
use labrinth::models::projects::ProjectId;
|
use labrinth::models::projects::ProjectId;
|
||||||
|
use labrinth::models::users::UserId;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
// For each scope, we (using test_scope):
|
// For each scope, we (using test_scope):
|
||||||
@@ -1185,8 +1186,13 @@ pub async fn organization_scopes() {
|
|||||||
|
|
||||||
// remove project (now that we've checked)
|
// remove project (now that we've checked)
|
||||||
let req_gen = |pat: Option<String>| async move {
|
let req_gen = |pat: Option<String>| async move {
|
||||||
api.organization_remove_project(organization_id, beta_project_id, pat.as_deref())
|
api.organization_remove_project(
|
||||||
.await
|
organization_id,
|
||||||
|
beta_project_id,
|
||||||
|
UserId(USER_USER_ID_PARSED as u64),
|
||||||
|
pat.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
};
|
};
|
||||||
ScopeTest::new(&test_env)
|
ScopeTest::new(&test_env)
|
||||||
.with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE)
|
.with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE)
|
||||||
|
|||||||
@@ -287,11 +287,12 @@ async fn test_get_team_project_orgs() {
|
|||||||
// The team members route from teams (on a project's team):
|
// The team members route from teams (on a project's team):
|
||||||
// - the members of the project team specifically
|
// - the members of the project team specifically
|
||||||
// - not the ones from the organization
|
// - not the ones from the organization
|
||||||
|
// - Remember: the owner of an org will not be included in the org's team members list
|
||||||
let members = test_env
|
let members = test_env
|
||||||
.api
|
.api
|
||||||
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(members.len(), 1);
|
assert_eq!(members.len(), 0);
|
||||||
|
|
||||||
// The team members route from project should show:
|
// The team members route from project should show:
|
||||||
// - the members of the project team including the ones from the organization
|
// - the members of the project team including the ones from the organization
|
||||||
|
|||||||
Reference in New Issue
Block a user