add custom slugs to orgs
All checks were successful
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Successful in 43s

This commit is contained in:
Braydon 2024-09-18 19:16:54 -04:00
parent a929a4ee48
commit a394ecc0e3
6 changed files with 50 additions and 9 deletions

@ -1,6 +1,8 @@
package cc.pulseapp.api.common; package cc.pulseapp.api.common;
import cc.pulseapp.api.model.IGenericResponse; import cc.pulseapp.api.model.IGenericResponse;
import cc.pulseapp.api.model.org.Organization;
import cc.pulseapp.api.model.user.User;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@ -17,9 +19,6 @@ public final class StringUtils {
private static final String SPECIAL_STRING = "!@#$%^&*()_+-=[]{}|;:,.<>?"; private static final String SPECIAL_STRING = "!@#$%^&*()_+-=[]{}|;:,.<>?";
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-z0-9_.]*$");
/** /**
* Check if the given email is valid. * Check if the given email is valid.
* *
@ -27,7 +26,7 @@ public final class StringUtils {
* @return whether the email is valid * @return whether the email is valid
*/ */
public static boolean isValidEmail(@NonNull String email) { public static boolean isValidEmail(@NonNull String email) {
return !email.isBlank() && EMAIL_PATTERN.matcher(email).matches(); return !email.isBlank() && User.EMAIL_PATTERN.matcher(email).matches();
} }
/** /**
@ -37,7 +36,17 @@ public final class StringUtils {
* @return whether the username is valid * @return whether the username is valid
*/ */
public static boolean isValidUsername(@NonNull String username) { public static boolean isValidUsername(@NonNull String username) {
return USERNAME_PATTERN.matcher(username).matches(); return User.USERNAME_PATTERN.matcher(username).matches();
}
/**
* Check if the given org slug is valid.
*
* @param slug the slug to check
* @return whether the slug is valid
*/
public static boolean isValidOrgSlug(@NonNull String slug) {
return Organization.SLUG_PATTERN.matcher(slug).matches();
} }
/** /**

@ -6,6 +6,8 @@ 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.regex.Pattern;
/** /**
* An organization owned by a {@link User}. * An organization owned by a {@link User}.
* *
@ -15,6 +17,8 @@ import org.springframework.data.mongodb.core.mapping.Document;
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
@Document("organizations") @Document("organizations")
public final class Organization { public final class Organization {
public static final Pattern SLUG_PATTERN = Pattern.compile("^[a-z0-9]+(?:-[a-z0-9]+)*$");
/** /**
* The snowflake id of this organization. * The snowflake id of this organization.
*/ */
@ -25,6 +29,11 @@ public final class Organization {
*/ */
@Indexed @NonNull private final String name; @Indexed @NonNull private final String name;
/**
* The slug of this organization.
*/
@Indexed @NonNull private final String slug;
/** /**
* The snowflake of the {@link User} * The snowflake of the {@link User}
* that owns this organization. * that owns this organization.

@ -6,6 +6,7 @@ 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.Date; import java.util.Date;
import java.util.regex.Pattern;
/** /**
* @author Braydon * @author Braydon
@ -14,6 +15,9 @@ import java.util.Date;
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
@Document("users") @Document("users")
public final class User { public final class User {
public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
public static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-z0-9_.]*$");
/** /**
* The snowflake id of this user. * The snowflake id of this user.
*/ */

@ -19,6 +19,11 @@ public final class CompleteOnboardingInput {
*/ */
private final String organizationName; private final String organizationName;
/**
* The slug of the {@link Organization} to create.
*/
private final String organizationSlug;
/** /**
* The name of the {@link StatusPage} to create. * The name of the {@link StatusPage} to create.
*/ */
@ -31,6 +36,7 @@ public final class CompleteOnboardingInput {
*/ */
public boolean isValid() { public boolean isValid() {
return organizationName != null && (!organizationName.isBlank()) return organizationName != null && (!organizationName.isBlank())
&& organizationSlug != null && (!organizationSlug.isBlank())
&& statusPageName != null && (!statusPageName.isBlank()); && statusPageName != null && (!statusPageName.isBlank());
} }
} }

@ -36,12 +36,13 @@ public final class OrganizationService {
* Create a new organization. * Create a new organization.
* *
* @param name the org name * @param name the org name
* @param slug the org slug
* @param owner the owner of the org * @param owner the owner of the org
* @return the created org * @return the created org
* @throws BadRequestException if the org creation fails * @throws BadRequestException if the org creation fails
*/ */
@NonNull @NonNull
public Organization createOrganization(@Nonnull String name, @NonNull User owner) throws BadRequestException { public Organization createOrganization(@Nonnull String name, @Nonnull String slug, @NonNull User owner) throws BadRequestException {
// Ensure org creation is enabled // Ensure org creation is enabled
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);
@ -51,7 +52,7 @@ public final class OrganizationService {
throw new BadRequestException(Error.ORG_NAME_TAKEN); throw new BadRequestException(Error.ORG_NAME_TAKEN);
} }
// Create the org and return it // Create the org and return it
return orgRepository.save(new Organization(snowflakeService.generateSnowflake(), name, owner.getSnowflake())); return orgRepository.save(new Organization(snowflakeService.generateSnowflake(), name, slug, owner.getSnowflake()));
} }
/** /**

@ -77,16 +77,27 @@ public final class UserService {
return StringUtils.isValidEmail(email) && userRepository.findByEmailIgnoreCase(email) != null; return StringUtils.isValidEmail(email) && userRepository.findByEmailIgnoreCase(email) != null;
} }
public void completeOnboarding(CompleteOnboardingInput input) { /**
* Complete the onboarding
* process for a user.
*
* @param input the input to process
* @throws BadRequestException if onboarding fails
*/
public void completeOnboarding(CompleteOnboardingInput input) throws BadRequestException {
// Ensure the input was provided // Ensure the input was provided
if (input == null || (!input.isValid())) { if (input == null || (!input.isValid())) {
throw new BadRequestException(Error.MALFORMED_ONBOARDING_INPUT); throw new BadRequestException(Error.MALFORMED_ONBOARDING_INPUT);
} }
// Ensure the org slug is valid
if (!StringUtils.isValidOrgSlug(input.getOrganizationSlug())) {
throw new BadRequestException(Error.ORGANIZATION_SLUG_INVALID);
}
User user = authService.getAuthenticatedUser(); User user = authService.getAuthenticatedUser();
if (user.hasFlag(UserFlag.COMPLETED_ONBOARDING)) { // Already completed if (user.hasFlag(UserFlag.COMPLETED_ONBOARDING)) { // Already completed
throw new BadRequestException(Error.ALREADY_ONBOARDED); throw new BadRequestException(Error.ALREADY_ONBOARDED);
} }
Organization org = orgService.createOrganization(input.getOrganizationName(), user); // Create the org Organization org = orgService.createOrganization(input.getOrganizationName(), input.getOrganizationSlug(), user); // Create the org
statusPageService.createStatusPage(input.getStatusPageName(), org); // Create the status page statusPageService.createStatusPage(input.getStatusPageName(), org); // Create the status page
user.addFlag(UserFlag.COMPLETED_ONBOARDING); // Flag completed onboarding user.addFlag(UserFlag.COMPLETED_ONBOARDING); // Flag completed onboarding
userRepository.save(user); userRepository.save(user);
@ -97,6 +108,7 @@ public final class UserService {
*/ */
private enum Error implements IGenericResponse { private enum Error implements IGenericResponse {
MALFORMED_ONBOARDING_INPUT, MALFORMED_ONBOARDING_INPUT,
ORGANIZATION_SLUG_INVALID,
ALREADY_ONBOARDED, ALREADY_ONBOARDED,
} }
} }