Change up the org backend a bit to support members
All checks were successful
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Successful in 52s

This commit is contained in:
Braydon 2024-09-20 03:21:27 -04:00
parent c5841402f3
commit 58b7b69bb3
8 changed files with 109 additions and 24 deletions

@ -21,7 +21,7 @@ public final class DetailedOrganization extends cc.pulseapp.api.model.org.Organi
@NonNull private final List<StatusPage> statusPages; @NonNull private final List<StatusPage> statusPages;
public DetailedOrganization(@NonNull cc.pulseapp.api.model.org.Organization origin, @NonNull List<StatusPage> statusPages) { public DetailedOrganization(@NonNull cc.pulseapp.api.model.org.Organization origin, @NonNull List<StatusPage> statusPages) {
super(origin.getSnowflake(), origin.getName(), origin.getSlug(), origin.getLogo(), origin.getOwnerSnowflake()); super(origin.getSnowflake(), origin.getName(), origin.getSlug(), origin.getLogo(), origin.getMembers(), origin.getOwnerSnowflake());
this.statusPages = statusPages; this.statusPages = statusPages;
} }
} }

@ -6,6 +6,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -39,6 +40,11 @@ public class Organization {
*/ */
private final String logo; private final String logo;
/**
* The members of this organization.
*/
@NonNull private final List<OrganizationMember> members;
/** /**
* The snowflake of the {@link User} * The snowflake of the {@link User}
* that owns this organization. * that owns this organization.

@ -0,0 +1,25 @@
package cc.pulseapp.api.model.org;
import cc.pulseapp.api.model.user.User;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* A member of an {@link Organization}.
*
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public final class OrganizationMember {
/**
* The snowflake of the {@link User}
* this member belongs to.
*/
private final long userSnowflake;
/**
* The bitwise permissions of this member.
*/
private final int permissions;
}

@ -0,0 +1,53 @@
package cc.pulseapp.api.model.org;
import cc.pulseapp.api.model.page.StatusPage;
/**
* The permissions of a {@link OrganizationMember}.
*
* @author Braydon
*/
public enum OrganizationMemberPermission {
/**
* The member can create, edit, and delete
* {@link StatusPage}s for the organization.
*/
MANAGE_STATUS_PAGES,
/**
* The member can create, edit, and
* delete automations for the organization.
*/
MANAGE_AUTOMATIONS,
/**
* The member can create, edit, and
* delete incidents for the organization.
*/
MANAGE_INCIDENTS,
/**
* The member can view insights of the organization.
*/
VIEW_INSIGHTS,
/**
* The member can view audit logs of the organization.
*/
VIEW_AUDIT_LOGS,
/**
* The member can edit the organization.
*/
MANAGE_ORGANIZATION;
/**
* Get the bitwise value
* of this permission.
*
* @return the bitwise value
*/
public int bitwise() {
return 1 << ordinal();
}
}

@ -60,7 +60,7 @@ public final class User {
private TFAProfile tfa; private TFAProfile tfa;
/** /**
* The flags for this user. * The bitwise flags for this user.
*/ */
private int flags; private int flags;

@ -3,6 +3,7 @@ package cc.pulseapp.api.repository;
import cc.pulseapp.api.model.org.Organization; import cc.pulseapp.api.model.org.Organization;
import lombok.NonNull; import lombok.NonNull;
import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
@ -15,12 +16,12 @@ import java.util.List;
@Repository @Repository
public interface OrganizationRepository extends MongoRepository<Organization, Long> { public interface OrganizationRepository extends MongoRepository<Organization, Long> {
/** /**
* Find an organization by its name. * Get the organization that has the given slug.
* *
* @param name the name of the org * @param slug the organization slug
* @return the org with the name * @return the organization, null if none
*/ */
Organization findByNameIgnoreCase(@NonNull String name); Organization findBySlug(@NonNull String slug);
/** /**
* Get the organizations that * Get the organizations that
@ -30,4 +31,14 @@ public interface OrganizationRepository extends MongoRepository<Organization, Lo
* @return the owned organizations * @return the owned organizations
*/ */
List<Organization> findByOwnerSnowflake(long ownerSnowflake); List<Organization> findByOwnerSnowflake(long ownerSnowflake);
/**
* Get the organizations that the user
* either owns or is a member of.
*
* @param userSnowflake the user's snowflake
* @return the organizations the user has access to
*/
@Query("{ '$or': [ { 'ownerSnowflake': ?0 }, { 'members.userSnowflake': ?0 } ] }")
List<Organization> findByUserAccess(long userSnowflake);
} }

@ -19,12 +19,4 @@ public interface UserRepository extends MongoRepository<User, Long> {
* @return the user with the email * @return the user with the email
*/ */
User findByEmailIgnoreCase(@NonNull String email); User findByEmailIgnoreCase(@NonNull String email);
/**
* Find a user by their username.
*
* @param username the username of the user
* @return the user with the username
*/
User findByUsernameIgnoreCase(@NonNull String username);
} }

@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -66,21 +67,18 @@ public final class OrganizationService {
if (!Feature.ORG_CREATION_ENABLED.isEnabled()) { if (!Feature.ORG_CREATION_ENABLED.isEnabled()) {
throw new BadRequestException(Error.ORG_CREATION_DISABLED); throw new BadRequestException(Error.ORG_CREATION_DISABLED);
} }
List<Organization> ownedOrgs = orgRepository.findByOwnerSnowflake(owner.getSnowflake()); // Ensure the org slug isn't taken
if (orgRepository.findBySlug(slug) != null) {
// Ensure the org name isn't taken throw new BadRequestException(Error.ORG_SLUG_TAKEN);
if (ownedOrgs.stream().anyMatch(org -> org.getName().equalsIgnoreCase(name))) {
throw new BadRequestException(Error.ORG_NAME_TAKEN);
} }
// Handle cloud environment checks // Handle cloud environment checks
if (EnvironmentUtils.isCloud()) { if (EnvironmentUtils.isCloud()) {
if (ownedOrgs.size() >= owner.getTier().getMaxOrganizations()) { if (orgRepository.findByOwnerSnowflake(owner.getSnowflake()).size() >= owner.getTier().getMaxOrganizations()) {
throw new BadRequestException(Error.MAX_ORGS_REACHED); throw new BadRequestException(Error.MAX_ORGS_REACHED);
} }
} }
// Create the org and return it // Create the org and return it
slug = slug.trim().replaceAll("-+$", ""); // Trim slug trailing dashes return orgRepository.save(new Organization(snowflakeService.generateSnowflake(), name, slug, null, Collections.emptyList(), owner.getSnowflake()));
return orgRepository.save(new Organization(snowflakeService.generateSnowflake(), name, slug, null, owner.getSnowflake()));
} }
/** /**
@ -93,7 +91,7 @@ public final class OrganizationService {
public List<DetailedOrganization> getOrganizations() { public List<DetailedOrganization> getOrganizations() {
User user = authService.getAuthenticatedUser(); User user = authService.getAuthenticatedUser();
List<DetailedOrganization> organizations = new ArrayList<>(); List<DetailedOrganization> organizations = new ArrayList<>();
for (Organization org : orgRepository.findByOwnerSnowflake(user.getSnowflake())) { for (Organization org : orgRepository.findByUserAccess(user.getSnowflake())) {
organizations.add(new DetailedOrganization(org, statusPageRepository.findByOrgSnowflake(org.getSnowflake()))); organizations.add(new DetailedOrganization(org, statusPageRepository.findByOrgSnowflake(org.getSnowflake())));
} }
return organizations; return organizations;
@ -104,7 +102,7 @@ public final class OrganizationService {
*/ */
private enum Error implements IGenericResponse { private enum Error implements IGenericResponse {
ORG_CREATION_DISABLED, ORG_CREATION_DISABLED,
ORG_NAME_TAKEN, ORG_SLUG_TAKEN,
MAX_ORGS_REACHED MAX_ORGS_REACHED
} }
} }