Team routes (#92)

* Team routes template

* More work on teams

* Updating routes WIP

* Edit routes

* Fixes

* Run prepare, prevent non-members from seeing perms

* More fixes

* Finish team routes

* More fixes

* Unpushed changes

* Some more fixes and error handling

* Fix sqlx prepare, formatting

Co-authored-by: Aeledfyr <aeledfyr@gmail.com>
This commit is contained in:
Geometrically
2020-11-09 19:39:23 -07:00
committed by GitHub
parent c8e58a1e5b
commit 578d673a4e
15 changed files with 1237 additions and 111 deletions

1
Cargo.lock generated
View File

@@ -1612,6 +1612,7 @@ dependencies = [
"actix-web",
"async-trait",
"base64 0.13.0",
"bitflags",
"chrono",
"dotenv",
"env_logger",

View File

@@ -27,6 +27,7 @@ chrono = { version = "0.4", features = ["serde"] }
rand = "0.7.3"
base64 = "0.13.0"
sha1 = { version = "0.6.0", features = ["std"] }
bitflags = "1.2.1"
gumdrop = "0.8.0"
dotenv = "0.15"

View File

@@ -0,0 +1,5 @@
-- Add migration script here
ALTER TABLE team_members
ADD COLUMN permissions bigint default 0 NOT NULL;
ALTER TABLE team_members
ADD COLUMN accepted boolean default false NOT NULL;

View File

@@ -1,25 +1,5 @@
{
"db": "PostgreSQL",
"02d8895627dfe108735a6e10ad63239348b71b3322b4734526a2646f17aedf05": {
"query": "\n SELECT status FROM statuses\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false
]
}
},
"03209c5bda2d704e688439919a7b3903db6ad7caebf7ddafb3ea52d312d47bfb": {
"query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n ",
"describe": {
@@ -39,6 +19,47 @@
"nullable": []
}
},
"0739834cfbef869855ed4e1aea7e1f7601f6519867ee48c573ee901c4498e04c": {
"query": "\n UPDATE team_members\n SET permissions = $1\n WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Text"
]
},
"nullable": []
}
},
"07e72d55c2f18744bbfffee9920866d4aacd680f316058ec734735c173a7f16b": {
"query": "\n SELECT DISTINCT gv.version, gv.created FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "created",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false
]
}
},
"0ca11a32b2860e4f5c3d20892a5be3cb419e084f42ba0f98e09b9995027fcc4e": {
"query": "\n SELECT id FROM statuses\n WHERE status = $1\n ",
"describe": {
@@ -194,22 +215,6 @@
]
}
},
"15b2a2f1bbbbab4f1d99e5e428b2ffba77c83814b936fa6e10e2703b207f6e9a": {
"query": "\n INSERT INTO team_members (id, team_id, user_id, member_name, role)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Varchar",
"Varchar"
]
},
"nullable": []
}
},
"17e6d30c3693e9bd9f772f3dc4e2eafe75fdeecfdcf2746eac641f77ced6b8a8": {
"query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role FROM users u\n WHERE u.id IN (SELECT * FROM UNNEST($1::bigint[]))\n ",
"describe": {
@@ -384,6 +389,56 @@
]
}
},
"21d268dbad5ffd34d476998aea4475cdb071e8cfbb245c4853ee5f4c44b0c8ae": {
"query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND accepted = TRUE)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "member_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "accepted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"225597042db9c2d95296ea6bbeda4e99ffc9ddfab3991c8637ac3f4749ece6f3": {
"query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id\n FROM mods m\n WHERE id = $1\n ",
"describe": {
@@ -547,6 +602,20 @@
"nullable": []
}
},
"3b52d9f68ba23d1e3764f8df9f28bcaec0741101f6afd0c7c234b7f1b91054a4": {
"query": "\n UPDATE team_members\n SET accepted = TRUE\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
}
},
"42e072309779598d0c213280dd8052d1b4889cb24ef5204ca13b74f693b94328": {
"query": "\n SELECT user_id FROM team_members tm\n INNER JOIN mods ON mods.team_id = tm.team_id\n WHERE mods.id = $1\n ",
"describe": {
@@ -826,6 +895,72 @@
"nullable": []
}
},
"6c2299a7b7ab22f83049bc41fb5dd380adea3579e7b00df7d16fb6747a0a7313": {
"query": "\n UPDATE team_members\n SET role = $1\n WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Int8",
"Text"
]
},
"nullable": []
}
},
"6d8f1863579977d367784a5b457e21c841886834afe69a964de039d6f92795d4": {
"query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "member_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "accepted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"71db1bc306ff6da3a92544e1585aa11c5627b50d95b15e794b2fa5dc838ea1a3": {
"query": "\n SELECT mod_id, version_number, author_id\n FROM versions\n WHERE id = $1\n ",
"describe": {
@@ -920,6 +1055,56 @@
]
}
},
"733a9569fef2aa48d1d3b1b02d7fa893174d523adc57fb22995b1ea8897f3abf": {
"query": "\n SELECT id, team_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (user_id = $1 AND accepted = TRUE)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "team_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "member_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "accepted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"73bdd6c9e7cd8c1ed582261aebdee0f8fd2734e712ef288a2608564c918009cb": {
"query": "\n DELETE FROM versions WHERE id = $1\n ",
"describe": {
@@ -952,20 +1137,8 @@
"nullable": []
}
},
"8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": {
"query": "\n DELETE FROM loaders\n WHERE loader = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
}
},
"91d7e437c6dfb9b95e68aca92154bd2af4de13eeb9611e08dcad2717d0c41ed9": {
"query": "\n SELECT id, user_id, member_name, role\n FROM team_members\n WHERE team_id = $1\n ",
"7bbbeecf3246a8e07ad073a07f7d057e0990a810d69ae18cec41de60b704b174": {
"query": "\n SELECT id, user_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)\n ",
"describe": {
"columns": [
{
@@ -987,14 +1160,27 @@
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "accepted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
@@ -1002,6 +1188,32 @@
]
}
},
"8ba2b2c38958f1c542e514fc62ab4682f58b0b442ac1842d20625420698e34ec": {
"query": "\n DELETE FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
]
},
"nullable": []
}
},
"8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": {
"query": "\n DELETE FROM loaders\n WHERE loader = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
}
},
"9a41d6c1d5c250df6114157edf5621a88bc336c5c628ba89182ba999e0af3ba8": {
"query": "\n SELECT id, title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ",
"describe": {
@@ -1121,6 +1333,24 @@
]
}
},
"9ef0577ee4845091a0d29bad6d1130223767e6d6140cafb9013fdb428d5159dd": {
"query": "\n INSERT INTO team_members (id, team_id, user_id, member_name, role, permissions, accepted)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Varchar",
"Varchar",
"Int8",
"Bool"
]
},
"nullable": []
}
},
"a2a99a640468a9fb8f0718e5aea6740cf5b33dafd5e038c154d6a13674fa999b": {
"query": "\n INSERT INTO mods (\n id, team_id, title, description, body_url,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12\n )\n ",
"describe": {
@@ -1262,6 +1492,32 @@
]
}
},
"ad273daadd249e93b500c339a62aac48a497ebcc15164776ad20860a4d232896": {
"query": "\n SELECT DISTINCT gv.version, gv.created FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "created",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false
]
}
},
"b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": {
"query": "\n DELETE FROM teams\n WHERE id = $1\n ",
"describe": {
@@ -1700,6 +1956,73 @@
]
}
},
"d99c8f5a2d8f73f6c91ac7e72e352e03e608522142aab1b569ef5eced5ec18f8": {
"query": "\n INSERT INTO team_members (\n id, user_id, member_name, role, permissions, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Varchar",
"Varchar",
"Int8",
"Bool"
]
},
"nullable": []
}
},
"dd16be7b85d2a4bd77eccc780d27031b964d4d346d8899b4dd0e2f47ba86d5fd": {
"query": "\n SELECT id, team_id, member_name, role, permissions, accepted\n FROM team_members\n WHERE user_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "team_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "member_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "accepted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"deb81673526789bca38d39e64303f61d2a63febfdfb68136e58517af9f7792bc": {
"query": "\n SELECT category FROM mods_categories\n INNER JOIN categories ON joining_category_id = id\n WHERE joining_mod_id = $1\n ",
"describe": {
@@ -1740,6 +2063,21 @@
]
}
},
"e53013562a3a9659df5c56185b92cf9cc30ce7e409f4762be98662161af593b9": {
"query": "\n UPDATE team_members\n SET member_name = $1\n WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Int8",
"Text"
]
},
"nullable": []
}
},
"e673006d1355fa91ba5739d7cf569eec5e1ec501f7b1dc2b431f0b1c25ac07d5": {
"query": "\n DELETE FROM game_versions\n WHERE version = $1\n ",
"describe": {

View File

@@ -30,6 +30,10 @@ pub enum DatabaseError {
alphanumeric characters or '_-'."
)]
InvalidIdentifier(String),
#[error("Invalid permissions bitflag!")]
BitflagError,
#[error("A database request failed")]
Other(String),
}
impl ids::ChannelId {

View File

@@ -1,4 +1,5 @@
use super::ids::*;
use crate::models::teams::Permissions;
pub struct TeamBuilder {
pub members: Vec<TeamMemberBuilder>,
@@ -7,6 +8,8 @@ pub struct TeamMemberBuilder {
pub user_id: UserId,
pub name: String,
pub role: String,
pub permissions: Permissions,
pub accepted: bool,
}
impl TeamBuilder {
@@ -36,18 +39,22 @@ impl TeamBuilder {
user_id: member.user_id,
name: member.name,
role: member.role,
permissions: member.permissions,
accepted: member.accepted,
};
sqlx::query!(
"
INSERT INTO team_members (id, team_id, user_id, member_name, role)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO team_members (id, team_id, user_id, member_name, role, permissions, accepted)
VALUES ($1, $2, $3, $4, $5, $6, $7)
",
team_member.id as TeamMemberId,
team_member.team_id as TeamId,
team_member.user_id as UserId,
team_member.name,
team_member.role,
team_member.permissions.bits() as i64,
team_member.accepted,
)
.execute(&mut *transaction)
.await?;
@@ -72,9 +79,12 @@ pub struct TeamMember {
/// The name of the user
pub name: String,
pub role: String,
pub permissions: Permissions,
pub accepted: bool,
}
impl TeamMember {
/// Lists the members of a team
pub async fn get_from_team<'a, 'b, E>(
id: TeamId,
executor: E,
@@ -86,25 +96,339 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, user_id, member_name, role
SELECT id, user_id, member_name, role, permissions, accepted
FROM team_members
WHERE team_id = $1
WHERE (team_id = $1 AND accepted = TRUE)
",
id as TeamId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
Ok(e.right().map(|m| TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id: UserId(m.user_id),
name: m.member_name,
role: m.role,
}))
if let Some(m) = e.right() {
let permissions = Permissions::from_bits(m.permissions as u64);
if let Some(perms) = permissions {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id: UserId(m.user_id),
name: m.member_name,
role: m.role,
permissions: perms,
accepted: m.accepted,
}))
} else {
Ok(None)
}
} else {
Ok(None)
}
})
.try_collect::<Vec<TeamMember>>()
.await?;
Ok(team_members)
}
/// Lists the team members for a user. Does not list pending requests.
pub async fn get_from_user_public<'a, 'b, E>(
id: UserId,
executor: E,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let team_members = sqlx::query!(
"
SELECT id, team_id, member_name, role, permissions, accepted
FROM team_members
WHERE (user_id = $1 AND accepted = TRUE)
",
id as UserId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
if let Some(m) = e.right() {
let permissions = Permissions::from_bits(m.permissions as u64);
if let Some(perms) = permissions {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
name: m.member_name,
role: m.role,
permissions: perms,
accepted: m.accepted,
}))
} else {
Ok(None)
}
} else {
Ok(None)
}
})
.try_collect::<Vec<TeamMember>>()
.await?;
Ok(team_members)
}
/// Lists the team members for a user. Includes pending requests.
pub async fn get_from_user_private<'a, 'b, E>(
id: UserId,
executor: E,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let team_members = sqlx::query!(
"
SELECT id, team_id, member_name, role, permissions, accepted
FROM team_members
WHERE user_id = $1
",
id as UserId,
)
.fetch_many(executor)
.try_filter_map(|e| async {
if let Some(m) = e.right() {
let permissions = Permissions::from_bits(m.permissions as u64);
if let Some(perms) = permissions {
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
name: m.member_name,
role: m.role,
permissions: perms,
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::BitflagError)))
}
} else {
Ok(None)
}
})
.try_collect::<Vec<Result<TeamMember, super::DatabaseError>>>()
.await?;
let team_members = team_members
.into_iter()
.collect::<Result<Vec<TeamMember>, super::DatabaseError>>()?;
Ok(team_members)
}
/// Gets a team member from a user id and team id. Does not return pending members.
pub async fn get_from_user_id<'a, 'b, E>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id, user_id, member_name, role, permissions, accepted
FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)
",
id as TeamId,
user_id as UserId
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id,
name: m.member_name,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or_else(|| super::DatabaseError::BitflagError)?,
accepted: m.accepted,
}))
} else {
Ok(None)
}
}
/// Gets a team member from a user id and team id, including pending members.
pub async fn get_from_user_id_pending<'a, 'b, E>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id, user_id, member_name, role, permissions, accepted
FROM team_members
WHERE (team_id = $1 AND user_id = $2)
",
id as TeamId,
user_id as UserId
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id,
name: m.member_name,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or_else(|| super::DatabaseError::BitflagError)?,
accepted: m.accepted,
}))
} else {
Ok(None)
}
}
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
sqlx::query!(
"
INSERT INTO team_members (
id, user_id, member_name, role, permissions, accepted
)
VALUES (
$1, $2, $3, $4, $5,
$6
)
",
self.id as TeamMemberId,
self.user_id as UserId,
self.name,
self.role,
self.permissions.bits() as i64,
self.accepted,
)
.execute(&mut *transaction)
.await?;
Ok(())
}
pub async fn delete<'a, 'b, E>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<(), super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use sqlx::Done;
let result = sqlx::query!(
"
DELETE FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)
",
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(executor)
.await?;
if result.rows_affected() != 1 {
return Err(super::DatabaseError::Other(format!(
"Deleting a member failed; {} rows deleted",
result.rows_affected()
)));
}
Ok(())
}
pub async fn edit_team_member(
id: TeamId,
user_id: UserId,
new_permissions: Option<Permissions>,
new_role: Option<String>,
new_accepted: Option<bool>,
new_name: Option<String>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
if let Some(permissions) = new_permissions {
sqlx::query!(
"
UPDATE team_members
SET permissions = $1
WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)
",
permissions.bits() as i64,
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(&mut *transaction)
.await?;
}
if let Some(role) = new_role {
sqlx::query!(
"
UPDATE team_members
SET role = $1
WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)
",
role,
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(&mut *transaction)
.await?;
}
if let Some(accepted) = new_accepted {
if accepted {
sqlx::query!(
"
UPDATE team_members
SET accepted = TRUE
WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)
",
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(&mut *transaction)
.await?;
}
}
if let Some(name) = new_name {
sqlx::query!(
"
UPDATE team_members
SET member_name = $1
WHERE (team_id = $2 AND user_id = $3 AND NOT role = $4)
",
name,
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(&mut *transaction)
.await?;
}
Ok(())
}
}

View File

@@ -20,6 +20,27 @@ pub struct Team {
pub members: Vec<TeamMember>,
}
bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Permissions: u64 {
const UPLOAD_VERSION = 1 << 0;
const DELETE_VERSION = 1 << 1;
const EDIT_DETAILS = 1 << 2;
const EDIT_BODY = 1 << 3;
const MANAGE_INVITES = 1 << 4;
const REMOVE_MEMBER = 1 << 5;
const EDIT_MEMBER = 1 << 6;
const DELETE_MOD = 1 << 7;
const ALL = 0b11111111;
}
}
impl Default for Permissions {
fn default() -> Permissions {
Permissions::UPLOAD_VERSION | Permissions::DELETE_VERSION
}
}
/// A member of a team
#[derive(Serialize, Deserialize, Clone)]
pub struct TeamMember {
@@ -29,4 +50,6 @@ pub struct TeamMember {
pub name: String,
/// The role of the user in the team
pub role: String,
/// A bitset containing the user's permissions in this team
pub permissions: Permissions,
}

View File

@@ -19,7 +19,7 @@ pub struct User {
pub role: Role,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Developer,

View File

@@ -49,12 +49,20 @@ pub fn users_config(cfg: &mut web::ServiceConfig) {
web::scope("user")
.service(users::user_get)
.service(users::mods_list)
.service(users::user_delete),
.service(users::user_delete)
.service(users::teams),
);
}
pub fn teams_config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("team").service(teams::team_members_get));
cfg.service(
web::scope("team")
.service(teams::team_members_get)
.service(teams::edit_team_member)
.service(teams::add_team_member)
.service(teams::join_team)
.service(teams::remove_team_member),
);
}
#[derive(thiserror::Error, Debug)]
@@ -63,8 +71,12 @@ pub enum ApiError {
DatabaseError(#[from] crate::database::models::DatabaseError),
#[error("Deserialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Authentication Error")]
AuthenticationError,
#[error("Authentication Error: {0}")]
AuthenticationError(#[from] crate::auth::AuthenticationError),
#[error("Authentication Error: {0}")]
CustomAuthenticationError(String),
#[error("Invalid Input: {0}")]
InvalidInputError(String),
#[error("Search Error: {0}")]
SearchError(#[from] meilisearch_sdk::errors::Error),
}
@@ -73,9 +85,11 @@ impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
ApiError::AuthenticationError => actix_web::http::StatusCode::UNAUTHORIZED,
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST,
ApiError::SearchError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
ApiError::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST,
}
}
@@ -84,9 +98,11 @@ impl actix_web::ResponseError for ApiError {
crate::models::error::ApiError {
error: match self {
ApiError::DatabaseError(..) => "database_error",
ApiError::AuthenticationError => "unauthorized",
ApiError::AuthenticationError(..) => "unauthorized",
ApiError::CustomAuthenticationError(..) => "unauthorized",
ApiError::JsonError(..) => "json_error",
ApiError::SearchError(..) => "search_error",
ApiError::InvalidInputError(..) => "invalid_input",
},
description: &self.to_string(),
},

View File

@@ -437,6 +437,8 @@ async fn mod_create_inner(
user_id: current_user.id.into(),
name: current_user.username.clone(),
role: crate::models::teams::OWNER_ROLE.to_owned(),
permissions: crate::models::teams::Permissions::ALL,
accepted: true,
}],
};

View File

@@ -1,8 +1,10 @@
use super::ApiError;
use crate::auth::check_is_moderator_from_headers;
use crate::auth::get_user_from_headers;
use crate::database;
use crate::models;
use crate::models::mods::SearchRequest;
use crate::models::teams::Permissions;
use crate::models::users::Role;
use crate::search::{search_for_mod, SearchConfig, SearchError};
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
@@ -92,17 +94,30 @@ pub async fn mod_delete(
pool: web::Data<PgPool>,
config: web::Data<SearchConfig>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(
req.headers(),
&mut *pool
.acquire()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0;
if user.role != Role::Moderator || user.role != Role::Admin {
let mod_item = database::models::Mod::get(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
let team_member = database::models::TeamMember::get_from_user_id(
mod_item.team_id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
if !team_member.permissions.contains(Permissions::DELETE_MOD) {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to delete this mod".to_string(),
));
}
}
let result = database::models::Mod::remove_full(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;

View File

@@ -41,8 +41,7 @@ pub async fn category_create(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = category.into_inner().0;
@@ -64,8 +63,7 @@ pub async fn category_delete(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = category.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
@@ -103,8 +101,7 @@ pub async fn loader_create(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = loader.into_inner().0;
@@ -126,8 +123,7 @@ pub async fn loader_delete(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = loader.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
@@ -187,8 +183,7 @@ pub async fn game_version_create(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = game_version.into_inner().0;
@@ -221,8 +216,7 @@ pub async fn game_version_delete(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let name = game_version.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;

View File

@@ -1,25 +1,363 @@
use crate::auth::get_user_from_headers;
use crate::database::models::TeamMember;
use crate::models::teams::TeamId;
use crate::models::teams::{Permissions, TeamId};
use crate::models::users::UserId;
use crate::routes::ApiError;
use actix_web::{get, web, HttpResponse};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[get("{id}/members")]
pub async fn team_members_get(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let members_data = TeamMember::get_from_team(id.into(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(user) = current_user {
let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.await
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() {
let team_members: Vec<crate::models::teams::TeamMember> = members_data
.into_iter()
.map(|data| crate::models::teams::TeamMember {
user_id: data.user_id.into(),
name: data.name,
role: data.role,
permissions: data.permissions,
})
.collect();
return Ok(HttpResponse::Ok().json(team_members));
}
}
let team_members: Vec<crate::models::teams::TeamMember> = members_data
.into_iter()
.map(|data| crate::models::teams::TeamMember {
user_id: data.user_id.into(),
name: data.name,
role: data.role,
permissions: Permissions::default(),
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
}
#[post("{id}/join")]
pub async fn join_team(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member =
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
if let Some(member) = member {
if member.accepted {
return Err(ApiError::InvalidInputError(
"You are already a member of this team".to_string(),
));
}
let mut transaction = pool
.begin()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
// Edit Team Member to set Accepted to True
TeamMember::edit_team_member(
team_id,
current_user.id.into(),
None,
None,
Some(true),
None,
&mut transaction,
)
.await?;
transaction
.commit()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
} else {
return Err(ApiError::InvalidInputError(
"There is no pending request from this team".to_string(),
));
}
Ok(HttpResponse::Ok().body(""))
}
fn default_role() -> String {
"Member".to_string()
}
#[derive(Serialize, Deserialize, Clone)]
pub struct NewTeamMember {
pub user_id: UserId,
#[serde(default = "default_role")]
pub role: String,
#[serde(default = "Permissions::default")]
pub permissions: Permissions,
}
#[post("{id}/members")]
pub async fn add_team_member(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_member: web::Json<NewTeamMember>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let mut transaction = pool
.begin()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let team_member =
TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?;
let member = match team_member {
Some(m) => m,
None => {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to invite users to this team".to_string(),
))
}
};
if !member.permissions.contains(Permissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to invite users to this team".to_string(),
));
}
if !member.permissions.contains(new_member.permissions) {
return Err(ApiError::InvalidInputError(
"The new member has permissions that you don't have".to_string(),
));
}
if new_member.role == crate::models::teams::OWNER_ROLE {
return Err(ApiError::InvalidInputError(
"The `Owner` role is restricted to one person".to_string(),
));
}
let request = crate::database::models::team_item::TeamMember::get_from_user_id_pending(
team_id,
member.user_id,
&**pool,
)
.await?;
if let Some(req) = request {
if req.accepted {
return Err(ApiError::InvalidInputError(
"The user is already a member of that team".to_string(),
));
} else {
return Err(ApiError::InvalidInputError(
"There is already a pending member request for this user".to_string(),
));
}
}
let new_user = crate::database::models::User::get(member.user_id, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?
.ok_or_else(|| ApiError::InvalidInputError("An invalid User ID specified".to_string()))?;
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
TeamMember {
id: new_id,
team_id,
user_id: new_member.user_id.into(),
name: new_user.username,
role: new_member.role.clone(),
permissions: new_member.permissions,
accepted: false,
}
.insert(&mut transaction)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
transaction
.commit()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
Ok(HttpResponse::Ok().body(""))
}
#[derive(Serialize, Deserialize, Clone)]
pub struct EditTeamMember {
pub permissions: Option<Permissions>,
pub role: Option<String>,
pub name: Option<String>,
}
#[patch("{id}/members/{user_id}")]
pub async fn edit_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
edit_member: web::Json<EditTeamMember>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let mut transaction = pool
.begin()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
let member = match team_member {
Some(m) => m,
None => {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to edit members of this team".to_string(),
))
}
};
// If the only thing being modified is the name, a user can
// modify their own member without extra permissions.
if user_id == current_user.id.into()
&& edit_member.permissions.is_none()
&& edit_member.role.is_none()
{
TeamMember::edit_team_member(
id,
user_id,
None,
None,
None,
edit_member.name.clone(),
&mut transaction,
)
.await?;
transaction
.commit()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
return Ok(HttpResponse::Ok().body(""));
}
if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to edit members of this team".to_string(),
));
}
if let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInputError(
"The new permissions have permissions that you don't have".to_string(),
));
}
}
if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) {
return Err(ApiError::InvalidInputError(
"The `Owner` role is restricted to one person".to_string(),
));
}
TeamMember::edit_team_member(
id,
user_id,
edit_member.permissions,
edit_member.role.clone(),
None,
edit_member.name.clone(),
&mut transaction,
)
.await?;
transaction
.commit()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
Ok(HttpResponse::Ok().body(""))
}
#[delete("{id}/members/{user_id}")]
pub async fn remove_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let member = match team_member {
Some(m) => m,
None => {
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to remove members from this team".to_string(),
))
}
};
let delete_member = TeamMember::get_from_user_id(id, user_id, &**pool).await?;
if let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE {
// The owner cannot be removed from a team
return Err(ApiError::CustomAuthenticationError(
"The owner can't be removed from a team".to_string(),
));
}
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id
|| member.permissions.contains(Permissions::REMOVE_MEMBER)
{
TeamMember::delete(id, user_id, &**pool).await?;
} else {
return Err(ApiError::CustomAuthenticationError(
"You do not have permission to remove a member from this team".to_string(),
));
}
} else if delete_member.user_id == member.user_id
|| member.permissions.contains(Permissions::MANAGE_INVITES)
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &**pool).await?;
} else {
return Err(ApiError::CustomAuthenticationError(
"You do not have permission to cancel a team invite".to_string(),
));
}
Ok(HttpResponse::Ok().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}

View File

@@ -1,5 +1,6 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::database::models::User;
use crate::database::models::{TeamMember, User};
use crate::models::teams::Permissions;
use crate::models::users::{Role, UserId};
use crate::routes::ApiError;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
@@ -19,8 +20,7 @@ pub async fn user_auth_get(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?,
.await?,
))
}
@@ -121,6 +121,47 @@ pub async fn mods_list(
}
}
#[get("teams")]
pub async fn teams(
req: HttpRequest,
info: web::Path<(UserId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id: crate::database::models::UserId = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
let results;
let mut same_user = false;
if let Some(user) = current_user {
if user.id.0 == id.0 as u64 {
results = TeamMember::get_from_user_private(id, &**pool).await?;
same_user = true;
} else {
results = TeamMember::get_from_user_public(id, &**pool).await?;
}
} else {
results = TeamMember::get_from_user_public(id, &**pool).await?;
}
let team_members: Vec<crate::models::teams::TeamMember> = results
.into_iter()
.map(|data| crate::models::teams::TeamMember {
user_id: data.user_id.into(),
name: data.name,
role: data.role,
permissions: if same_user {
data.permissions
} else {
Permissions::default()
},
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
}
// TODO: Make this actually do stuff
#[delete("{id}")]
pub async fn user_delete(
@@ -135,8 +176,7 @@ pub async fn user_delete(
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
.await?;
let _id = info.0;
let result = Some(());

View File

@@ -1,7 +1,9 @@
use super::ApiError;
use crate::auth::check_is_moderator_from_headers;
use crate::auth::get_user_from_headers;
use crate::database;
use crate::models;
use crate::models::teams::Permissions;
use crate::models::users::Role;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -142,21 +144,44 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
#[delete("{version_id}")]
pub async fn version_delete(
req: HttpRequest,
info: web::Path<(models::ids::ModId, models::ids::VersionId)>,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(
req.headers(),
&mut *pool
.acquire()
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?,
)
.await
.map_err(|_| ApiError::AuthenticationError)?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0;
if user.role != Role::Moderator || user.role != Role::Admin {
let version = database::models::Version::get(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?
.ok_or_else(|| {
ApiError::InvalidInputError("Invalid Version ID specified!".to_string())
})?;
let mod_item = database::models::Mod::get(version.mod_id, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?
.ok_or_else(|| {
ApiError::InvalidInputError("Invalid Version ID specified!".to_string())
})?;
let team_member = database::models::TeamMember::get_from_user_id(
mod_item.team_id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.ok_or_else(|| ApiError::InvalidInputError("Invalid Version ID specified!".to_string()))?;
if !team_member
.permissions
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthenticationError(
"You don't have permission to delete versions in this team".to_string(),
));
}
}
// TODO: check if the mod exists and matches the version id
let id = info.1;
let result = database::models::Version::remove_full(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;