Initial work on payouts (badges, perms, splits) (#440)

* Initial work on payouts (badges, perms, splits)

* Fix clippy error, bitflag consistency
This commit is contained in:
Geometrically
2022-09-02 12:38:58 -07:00
committed by GitHub
parent 4c1dca73c4
commit e7c3f8bf47
13 changed files with 1030 additions and 801 deletions

View File

@@ -0,0 +1,7 @@
ALTER TABLE team_members ADD COLUMN payouts_split REAL NOT NULL DEFAULT 0;
UPDATE team_members
SET permissions = 1023, payouts_split = 100
WHERE role = 'Owner';
ALTER TABLE users ADD COLUMN badges bigint default 0 NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -28,8 +28,6 @@ pub enum DatabaseError {
Database(#[from] sqlx::error::Error),
#[error("Error while trying to generate random ID")]
RandomId,
#[error("Invalid permissions bitflag!")]
Bitflag,
#[error("A database request failed")]
Other(String),
}

View File

@@ -1,6 +1,7 @@
use super::ids::*;
use crate::database::models::User;
use crate::models::teams::Permissions;
use crate::models::users::Badges;
pub struct TeamBuilder {
pub members: Vec<TeamMemberBuilder>,
@@ -10,6 +11,7 @@ pub struct TeamMemberBuilder {
pub role: String,
pub permissions: Permissions,
pub accepted: bool,
pub payouts_split: f32,
}
impl TeamBuilder {
@@ -41,6 +43,7 @@ impl TeamBuilder {
role: member.role,
permissions: member.permissions,
accepted: member.accepted,
payouts_split: member.payouts_split,
};
sqlx::query!(
@@ -78,6 +81,7 @@ pub struct TeamMember {
pub role: String,
pub permissions: Permissions,
pub accepted: bool,
pub payouts_split: f32,
}
/// A member of a team
@@ -89,6 +93,7 @@ pub struct QueryTeamMember {
pub role: String,
pub permissions: Permissions,
pub accepted: bool,
pub payouts_split: f32,
}
impl TeamMember {
@@ -104,7 +109,7 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, user_id, role, permissions, accepted
SELECT id, user_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE team_id = $1
",
@@ -113,19 +118,16 @@ impl TeamMember {
.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: id,
user_id: UserId(m.user_id),
role: m.role,
permissions: perms,
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id: UserId(m.user_id),
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
})))
} else {
Ok(None)
}
@@ -152,10 +154,10 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted,
SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,
u.id user_id, u.github_id github_id, u.name user_name, u.email email,
u.avatar_url avatar_url, u.username username, u.bio bio,
u.created created, u.role user_role
u.created created, u.role user_role, u.badges badges
FROM team_members tm
INNER JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = $1
@@ -165,13 +167,12 @@ impl TeamMember {
.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(QueryTeamMember {
id: TeamMemberId(m.id),
team_id: id,
role: m.member_role,
permissions: perms,
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
accepted: m.accepted,
user: User {
id: UserId(m.user_id),
@@ -183,11 +184,10 @@ impl TeamMember {
bio: m.bio,
created: m.created,
role: m.user_role,
badges: Badges::from_bits(m.badges as u64).unwrap_or_default(),
},
payouts_split: m.payouts_split
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
}
@@ -216,10 +216,10 @@ impl TeamMember {
let teams = sqlx::query!(
"
SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted,
SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,
u.id user_id, u.github_id github_id, u.name user_name, u.email email,
u.avatar_url avatar_url, u.username username, u.bio bio,
u.created created, u.role user_role
u.created created, u.role user_role, u.badges badges
FROM team_members tm
INNER JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ANY($1)
@@ -230,13 +230,12 @@ impl TeamMember {
.fetch_many(exec)
.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(QueryTeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
role: m.member_role,
permissions: perms,
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
accepted: m.accepted,
user: User {
id: UserId(m.user_id),
@@ -248,11 +247,10 @@ impl TeamMember {
bio: m.bio,
created: m.created,
role: m.user_role,
badges: Badges::from_bits(m.badges as u64).unwrap_or_default(),
},
payouts_split: m.payouts_split
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
}
@@ -279,7 +277,7 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, team_id, role, permissions, accepted
SELECT id, team_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE (user_id = $1 AND accepted = TRUE)
",
@@ -288,19 +286,16 @@ impl TeamMember {
.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,
role: m.role,
permissions: perms,
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
})))
} else {
Ok(None)
}
@@ -327,7 +322,7 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, team_id, role, permissions, accepted
SELECT id, team_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE user_id = $1
",
@@ -336,19 +331,16 @@ impl TeamMember {
.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,
role: m.role,
permissions: perms,
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
Ok(Some(Ok(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id: id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
})))
} else {
Ok(None)
}
@@ -374,7 +366,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT id, user_id, role, permissions, accepted
SELECT id, user_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)
",
@@ -391,8 +383,9 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::Bitflag)?,
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
}))
} else {
Ok(None)
@@ -415,7 +408,7 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, team_id, user_id, role, permissions, accepted
SELECT id, team_id, user_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE (team_id = ANY($1) AND user_id = $2 AND accepted = TRUE)
",
@@ -425,19 +418,15 @@ impl TeamMember {
.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,
role: m.role,
permissions: perms,
permissions: Permissions::from_bits(m.permissions as u64).unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split
})))
} else {
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
}
@@ -463,7 +452,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT id, user_id, role, permissions, accepted
SELECT id, user_id, role, permissions, accepted, payouts_split
FROM team_members
WHERE (team_id = $1 AND user_id = $2)
",
@@ -480,8 +469,9 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::Bitflag)?,
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
}))
} else {
Ok(None)
@@ -550,6 +540,7 @@ impl TeamMember {
new_permissions: Option<Permissions>,
new_role: Option<String>,
new_accepted: Option<bool>,
new_payouts_split: Option<f32>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
if let Some(permissions) = new_permissions {
@@ -598,6 +589,21 @@ impl TeamMember {
}
}
if let Some(payouts_split) = new_payouts_split {
sqlx::query!(
"
UPDATE team_members
SET payouts_split = $1
WHERE (team_id = $2 AND user_id = $3)
",
payouts_split,
id as TeamId,
user_id as UserId,
)
.execute(&mut *transaction)
.await?;
}
Ok(())
}
@@ -611,7 +617,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM mods m
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted, tm.payouts_split FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE
WHERE m.id = $1
",
@@ -628,8 +634,9 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::Bitflag)?,
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
}))
} else {
Ok(None)
@@ -646,7 +653,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM versions v
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted, tm.payouts_split FROM versions v
INNER JOIN mods m ON m.id = v.mod_id
INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE
WHERE v.id = $1
@@ -664,8 +671,9 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::Bitflag)?,
.unwrap_or_default(),
accepted: m.accepted,
payouts_split: m.payouts_split,
}))
} else {
Ok(None)

View File

@@ -1,4 +1,5 @@
use super::ids::{ProjectId, UserId};
use crate::models::users::Badges;
use chrono::{DateTime, Utc};
pub struct User {
@@ -11,6 +12,7 @@ pub struct User {
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: String,
pub badges: Badges,
}
impl User {
@@ -54,7 +56,7 @@ impl User {
"
SELECT u.github_id, u.name, u.email,
u.avatar_url, u.username, u.bio,
u.created, u.role
u.created, u.role, u.badges
FROM users u
WHERE u.id = $1
",
@@ -74,6 +76,8 @@ impl User {
bio: row.bio,
created: row.created,
role: row.role,
badges: Badges::from_bits(row.badges as u64)
.unwrap_or_default(),
}))
} else {
Ok(None)
@@ -91,7 +95,7 @@ impl User {
"
SELECT u.id, u.name, u.email,
u.avatar_url, u.username, u.bio,
u.created, u.role
u.created, u.role, u.badges
FROM users u
WHERE u.github_id = $1
",
@@ -111,6 +115,8 @@ impl User {
bio: row.bio,
created: row.created,
role: row.role,
badges: Badges::from_bits(row.badges as u64)
.unwrap_or_default(),
}))
} else {
Ok(None)
@@ -128,7 +134,7 @@ impl User {
"
SELECT u.id, u.github_id, u.name, u.email,
u.avatar_url, u.username, u.bio,
u.created, u.role
u.created, u.role, u.badges
FROM users u
WHERE LOWER(u.username) = LOWER($1)
",
@@ -148,6 +154,8 @@ impl User {
bio: row.bio,
created: row.created,
role: row.role,
badges: Badges::from_bits(row.badges as u64)
.unwrap_or_default(),
}))
} else {
Ok(None)
@@ -169,7 +177,8 @@ impl User {
"
SELECT u.id, u.github_id, u.name, u.email,
u.avatar_url, u.username, u.bio,
u.created, u.role FROM users u
u.created, u.role, u.badges
FROM users u
WHERE u.id = ANY($1)
",
&user_ids_parsed
@@ -186,6 +195,7 @@ impl User {
bio: u.bio,
created: u.created,
role: u.role,
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
}))
})
.try_collect::<Vec<User>>()

View File

@@ -34,7 +34,9 @@ bitflags::bitflags! {
const REMOVE_MEMBER = 1 << 5;
const EDIT_MEMBER = 1 << 6;
const DELETE_PROJECT = 1 << 7;
const ALL = 0b11111111;
const VIEW_ANALYTICS = 1 << 8;
const VIEW_PAYOUTS = 1 << 9;
const ALL = 0b1111111111;
}
}
@@ -57,6 +59,9 @@ pub struct TeamMember {
pub permissions: Option<Permissions>,
/// Whether the user has joined the team or is just invited to it
pub accepted: bool,
/// Payouts split. This is a weighted average. For example. if a team has two members with this
/// value set to 25.0 for both members, they split revenue 50/50
pub payouts_split: Option<f32>,
}
impl TeamMember {
@@ -71,6 +76,11 @@ impl TeamMember {
Some(data.permissions)
},
accepted: data.accepted,
payouts_split: if override_permissions {
None
} else {
Some(data.payouts_split)
},
}
}
}

View File

@@ -9,6 +9,29 @@ pub struct UserId(pub u64);
pub const DELETED_USER: UserId = UserId(127155982985829);
bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct Badges: u64 {
const MIDAS = 1 << 0;
const EARLY_MODPACK_ADOPTER = 1 << 1;
const EARLY_RESPACK_ADOPTER = 1 << 2;
const EARLY_PLUGIN_ADOPTER = 1 << 3;
const ALPHA_TESTER = 1 << 4;
const CONTRIBUTOR = 1 << 5;
const TRANSLATOR = 1 << 6;
const ALL = 0b1111111;
const NONE = 0b0;
}
}
impl Default for Badges {
fn default() -> Badges {
Badges::NONE
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct User {
pub id: UserId,
@@ -20,6 +43,7 @@ pub struct User {
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: Role,
pub badges: Badges,
}
use crate::database::models::user_item::User as DBUser;
@@ -35,6 +59,7 @@ impl From<DBUser> for User {
bio: data.bio,
created: data.created,
role: Role::from_string(&*data.role),
badges: data.badges,
}
}
}

View File

@@ -15,7 +15,7 @@ use crate::database::models::{generate_state_id, User};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::DecodingError;
use crate::models::users::Role;
use crate::models::users::{Badges, Role};
use crate::parse_strings_from_var;
use crate::util::auth::get_github_user_from_token;
use actix_web::http::StatusCode;
@@ -272,6 +272,7 @@ pub async fn auth_callback(
bio: user.bio,
created: Utc::now(),
role: Role::Developer.to_string(),
badges: Badges::default(),
}
.insert(&mut transaction)
.await?;

View File

@@ -628,6 +628,7 @@ pub async fn project_create_inner(
role: crate::models::teams::OWNER_ROLE.to_owned(),
permissions: crate::models::teams::Permissions::ALL,
accepted: true,
payouts_split: 100.0,
}],
};

View File

@@ -189,6 +189,7 @@ pub async fn join_team(
None,
None,
Some(true),
None,
&mut transaction,
)
.await?;
@@ -214,6 +215,8 @@ pub struct NewTeamMember {
pub role: String,
#[serde(default = "Permissions::default")]
pub permissions: Permissions,
#[serde(default)]
pub payouts_split: f32,
}
#[post("{id}/members")]
@@ -255,6 +258,13 @@ pub async fn add_team_member(
"The `Owner` role is restricted to one person".to_string(),
));
}
if !(0.0..=5000.0).contains(&new_member.payouts_split) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
}
let request = crate::database::models::team_item::TeamMember::get_from_user_id_pending(
team_id,
new_member.user_id.into(),
@@ -291,6 +301,7 @@ pub async fn add_team_member(
role: new_member.role.clone(),
permissions: new_member.permissions,
accepted: false,
payouts_split: new_member.payouts_split,
}
.insert(&mut transaction)
.await?;
@@ -349,6 +360,7 @@ pub async fn add_team_member(
pub struct EditTeamMember {
pub permissions: Option<Permissions>,
pub role: Option<String>,
pub payouts_split: Option<f32>,
}
#[patch("{id}/members/{user_id}")]
@@ -406,6 +418,14 @@ pub async fn edit_team_member(
}
}
if let Some(payouts_split) = edit_member.payouts_split {
if !(0.0..=5000.0).contains(&payouts_split) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
}
}
if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) {
return Err(ApiError::InvalidInput(
"The `Owner` role is restricted to one person".to_string(),
@@ -418,6 +438,7 @@ pub async fn edit_team_member(
edit_member.permissions,
edit_member.role.clone(),
None,
edit_member.payouts_split,
&mut transaction,
)
.await?;
@@ -491,6 +512,7 @@ pub async fn transfer_ownership(
None,
Some(crate::models::teams::DEFAULT_ROLE.to_string()),
None,
None,
&mut transaction,
)
.await?;
@@ -501,6 +523,7 @@ pub async fn transfer_ownership(
Some(Permissions::ALL),
Some(crate::models::teams::OWNER_ROLE.to_string()),
None,
None,
&mut transaction,
)
.await?;

View File

@@ -2,7 +2,7 @@ use crate::database::models::User;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
use crate::models::projects::{Project, ProjectStatus};
use crate::models::users::{Role, UserId};
use crate::models::users::{Badges, Role, UserId};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_payload;
@@ -154,6 +154,7 @@ pub struct EditUser {
#[validate(length(max = 160))]
pub bio: Option<Option<String>>,
pub role: Option<Role>,
pub badges: Option<Badges>,
}
#[patch("{id}")]
@@ -277,6 +278,27 @@ pub async fn user_edit(
.await?;
}
if let Some(badges) = &new_user.badges {
if !user.role.is_admin() {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the badges of this user!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE users
SET badges = $1
WHERE (id = $2)
",
badges.bits() as i64,
id as crate::database::models::ids::UserId,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {

View File

@@ -72,6 +72,7 @@ where
bio: result.bio,
created: result.created,
role: Role::from_string(&result.role),
badges: result.badges,
}),
None => Err(AuthenticationError::InvalidCredentials),
}

View File

@@ -8,7 +8,7 @@ pub struct LiteLoaderValidator;
impl super::Validator for LiteLoaderValidator {
fn get_file_extensions(&self) -> &[&str] {
&["litemod"]
&["litemod", "jar"]
}
fn get_project_types(&self) -> &[&str] {