Organizations (#712)

* untested, unformatted, un-refactored

* minor simplification

* simplification fix

* refactoring, changes

* some fixes

* fixes, refactoring

* missed cache

* revs

* revs - more!

* removed donation links; added all org members to route

* renamed slug to title

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere
2023-10-02 10:56:57 -07:00
committed by GitHub
parent 58a61051b9
commit a1b59d4545
24 changed files with 3658 additions and 979 deletions

View File

@@ -3,6 +3,7 @@ use thiserror::Error;
pub use super::collections::CollectionId;
pub use super::images::ImageId;
pub use super::notifications::NotificationId;
pub use super::organizations::OrganizationId;
pub use super::pats::PatId;
pub use super::projects::{ProjectId, VersionId};
pub use super::reports::ReportId;
@@ -113,6 +114,7 @@ base62_id_impl!(UserId, UserId);
base62_id_impl!(VersionId, VersionId);
base62_id_impl!(CollectionId, CollectionId);
base62_id_impl!(TeamId, TeamId);
base62_id_impl!(OrganizationId, OrganizationId);
base62_id_impl!(ReportId, ReportId);
base62_id_impl!(NotificationId, NotificationId);
base62_id_impl!(ThreadId, ThreadId);

View File

@@ -4,6 +4,7 @@ pub mod error;
pub mod ids;
pub mod images;
pub mod notifications;
pub mod organizations;
pub mod pack;
pub mod pats;
pub mod projects;

View File

@@ -1,4 +1,5 @@
use super::ids::Base62Id;
use super::ids::OrganizationId;
use super::users::UserId;
use crate::database::models::notification_item::Notification as DBNotification;
use crate::database::models::notification_item::NotificationAction as DBNotificationAction;
@@ -42,6 +43,12 @@ pub enum NotificationBody {
invited_by: UserId,
role: String,
},
OrganizationInvite {
organization_id: OrganizationId,
invited_by: UserId,
team_id: TeamId,
role: String,
},
StatusChange {
project_id: ProjectId,
old_status: ProjectStatus,
@@ -105,6 +112,36 @@ impl From<DBNotification> for Notification {
},
],
),
NotificationBody::OrganizationInvite {
organization_id,
role,
team_id,
..
} => (
Some("organization_invite".to_string()),
"You have been invited to join an organization!".to_string(),
format!(
"An invite has been sent for you to be {} of an organization",
role
),
format!("/organization/{}", organization_id),
vec![
NotificationAction {
title: "Accept".to_string(),
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
},
NotificationAction {
title: "Deny".to_string(),
action_route: (
"DELETE".to_string(),
format!(
"organization/{organization_id}/members/{}",
UserId::from(notif.user_id)
),
),
},
],
),
NotificationBody::StatusChange {
old_status,
new_status,

View File

@@ -0,0 +1,49 @@
use super::{
ids::{Base62Id, TeamId},
teams::TeamMember,
};
use serde::{Deserialize, Serialize};
/// The ID of a team
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct OrganizationId(pub u64);
/// An organization of users who control a project
#[derive(Serialize, Deserialize)]
pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
/// The title (and slug) of the organization
pub title: String,
/// The associated team of the organization
pub team_id: TeamId,
/// The description of the organization
pub description: String,
/// The icon url of the organization
pub icon_url: Option<String>,
/// The color of the organization (picked from the icon)
pub color: Option<u32>,
/// A list of the members of the organization
pub members: Vec<TeamMember>,
}
impl Organization {
pub fn from(
data: crate::database::models::organization_item::Organization,
team_members: Vec<TeamMember>,
) -> Self {
Self {
id: data.id.into(),
title: data.title,
team_id: data.team_id.into(),
description: data.description,
members: team_members,
icon_url: data.icon_url,
color: data.color,
}
}
}

View File

@@ -94,8 +94,17 @@ bitflags::bitflags! {
// delete a collection
const COLLECTION_DELETE = 1 << 34;
const ALL = 0b11111111111111111111111111111111111;
const NOT_RESTRICTED = 0b111100000011111111111111100111;
// create an organization
const ORGANIZATION_CREATE = 1 << 35;
// read a user's organizations
const ORGANIZATION_READ = 1 << 36;
// write to an organization
const ORGANIZATION_WRITE = 1 << 37;
// delete an organization
const ORGANIZATION_DELETE = 1 << 38;
const ALL = 0b111111111111111111111111111111111111111;
const NOT_RESTRICTED = 0b1111111100000011111111111111100111;
const NONE = 0b0;
}
}

View File

@@ -1,4 +1,4 @@
use super::ids::Base62Id;
use super::ids::{Base62Id, OrganizationId};
use super::teams::TeamId;
use super::users::UserId;
use crate::database::models::project_item::QueryProject;
@@ -31,6 +31,8 @@ pub struct Project {
pub project_type: String,
/// The team of people that has ownership of this project.
pub team: TeamId,
/// The optional organization of people that have ownership of this project.
pub organization: Option<OrganizationId>,
/// The title or name of the project.
pub title: String,
/// A short description of the project.
@@ -120,6 +122,7 @@ impl From<QueryProject> for Project {
slug: m.slug,
project_type: data.project_type,
team: m.team_id.into(),
organization: m.organization_id.map(|i| i.into()),
title: m.title,
description: m.description,
body: m.body,

View File

@@ -24,7 +24,7 @@ pub struct Team {
bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct Permissions: u64 {
pub struct ProjectPermissions: u64 {
const UPLOAD_VERSION = 1 << 0;
const DELETE_VERSION = 1 << 1;
const EDIT_DETAILS = 1 << 2;
@@ -40,26 +40,86 @@ bitflags::bitflags! {
}
}
impl Default for Permissions {
fn default() -> Permissions {
Permissions::UPLOAD_VERSION | Permissions::DELETE_VERSION
impl Default for ProjectPermissions {
fn default() -> ProjectPermissions {
ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION
}
}
impl Permissions {
impl ProjectPermissions {
pub fn get_permissions_by_role(
role: &crate::models::users::Role,
project_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the project
organization_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the organization
) -> Option<Self> {
if role.is_admin() {
return Some(ProjectPermissions::ALL);
}
if let Some(member) = project_team_member {
return Some(member.permissions);
}
if let Some(member) = organization_team_member {
return Some(member.permissions); // Use default project permissions for the organization team member
}
if role.is_mod() {
Some(
ProjectPermissions::EDIT_DETAILS
| ProjectPermissions::EDIT_BODY
| ProjectPermissions::UPLOAD_VERSION,
)
} else {
None
}
}
}
bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct OrganizationPermissions: u64 {
const EDIT_DETAILS = 1 << 0;
const EDIT_BODY = 1 << 1;
const MANAGE_INVITES = 1 << 2;
const REMOVE_MEMBER = 1 << 3;
const EDIT_MEMBER = 1 << 4;
const ADD_PROJECT = 1 << 5;
const REMOVE_PROJECT = 1 << 6;
const DELETE_ORGANIZATION = 1 << 8;
const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 9; // Separate from EDIT_MEMBER
const ALL = 0b1111111111;
const NONE = 0b0;
}
}
impl Default for OrganizationPermissions {
fn default() -> OrganizationPermissions {
OrganizationPermissions::NONE
}
}
impl OrganizationPermissions {
pub fn get_permissions_by_role(
role: &crate::models::users::Role,
team_member: &Option<crate::database::models::TeamMember>,
) -> Option<Self> {
if role.is_admin() {
Some(Permissions::ALL)
} else if let Some(member) = team_member {
Some(member.permissions)
} else if role.is_mod() {
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY | Permissions::UPLOAD_VERSION)
} else {
None
return Some(OrganizationPermissions::ALL);
}
if let Some(member) = team_member {
return member.organization_permissions;
}
if role.is_mod() {
return Some(
OrganizationPermissions::EDIT_DETAILS
| OrganizationPermissions::EDIT_BODY
| OrganizationPermissions::ADD_PROJECT,
);
}
None
}
}
@@ -72,8 +132,16 @@ pub struct TeamMember {
pub user: User,
/// The role of the user in the team
pub role: String,
/// A bitset containing the user's permissions in this team
pub permissions: Option<Permissions>,
/// A bitset containing the user's permissions in this team.
/// In an organization-controlled project, these are the unique overriding permissions for the user's role for any project in the organization, if they exist.
/// In an organization, these are the default project permissions for any project in the organization.
/// Not optional- only None if they are being hidden from the user.
pub permissions: Option<ProjectPermissions>,
/// A bitset containing the user's permissions in this organization.
/// In a project team, this is None.
pub organization_permissions: Option<OrganizationPermissions>,
/// Whether the user has joined the team or is just invited to it
pub accepted: bool,
@@ -92,7 +160,17 @@ impl TeamMember {
override_permissions: bool,
) -> Self {
let user: User = user.into();
Self::from_model(data, user, override_permissions)
}
// Use the User model directly instead of the database model,
// if already available.
// (Avoids a db query in some cases)
pub fn from_model(
data: crate::database::models::team_item::TeamMember,
user: crate::models::users::User,
override_permissions: bool,
) -> Self {
Self {
team_id: data.team_id.into(),
user,
@@ -102,6 +180,11 @@ impl TeamMember {
} else {
Some(data.permissions)
},
organization_permissions: if override_permissions {
None
} else {
data.organization_permissions
},
accepted: data.accepted,
payouts_split: if override_permissions {
None