diff --git a/pom.xml b/pom.xml index 2a39a8b..25212e7 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,12 @@ 1.1 compile + + com.flagsmith + flagsmith-java-client + 7.2.0 + compile + diff --git a/src/main/java/cc/pulseapp/api/controller/v1/UserController.java b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java index 459dfb9..aa8c5db 100644 --- a/src/main/java/cc/pulseapp/api/controller/v1/UserController.java +++ b/src/main/java/cc/pulseapp/api/controller/v1/UserController.java @@ -1,6 +1,8 @@ package cc.pulseapp.api.controller.v1; +import cc.pulseapp.api.exception.impl.BadRequestException; import cc.pulseapp.api.model.user.UserDTO; +import cc.pulseapp.api.model.user.input.CompleteOnboardingInput; import cc.pulseapp.api.service.UserService; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; @@ -44,4 +46,10 @@ public final class UserController { public ResponseEntity> doesUserExist(@RequestParam @NonNull String email) { return ResponseEntity.ok(Map.of("exists", userService.doesUserExist(email))); } + + @PostMapping("/complete-onboarding") @ResponseBody @NonNull + public ResponseEntity> completeOnboarding(CompleteOnboardingInput input) throws BadRequestException { + userService.completeOnboarding(input); + return ResponseEntity.ok(Map.of("success", true)); + } } \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/user/UserFlag.java b/src/main/java/cc/pulseapp/api/model/user/UserFlag.java index de40f1c..8ced93c 100644 --- a/src/main/java/cc/pulseapp/api/model/user/UserFlag.java +++ b/src/main/java/cc/pulseapp/api/model/user/UserFlag.java @@ -14,5 +14,10 @@ public enum UserFlag { /** * The user completed the onboarding process. */ - COMPLETED_ONBOARDING + COMPLETED_ONBOARDING, + + /** + * The user is an administrator. + */ + ADMINISTRATOR } \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/user/input/CompleteOnboardingInput.java b/src/main/java/cc/pulseapp/api/model/user/input/CompleteOnboardingInput.java new file mode 100644 index 0000000..3758e8a --- /dev/null +++ b/src/main/java/cc/pulseapp/api/model/user/input/CompleteOnboardingInput.java @@ -0,0 +1,36 @@ +package cc.pulseapp.api.model.user.input; + +import cc.pulseapp.api.model.org.Organization; +import cc.pulseapp.api.model.page.StatusPage; +import cc.pulseapp.api.model.user.User; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * The input to complete onboarding for a {@link User}. + * + * @author Braydon + */ +@AllArgsConstructor @Getter @ToString +public final class CompleteOnboardingInput { + /** + * The name of the {@link Organization} to create. + */ + private final String organizationName; + + /** + * The name of the {@link StatusPage} to create. + */ + private final String statusPageName; + + /** + * Check if this input is valid. + * + * @return whether this input is valid + */ + public boolean isValid() { + return organizationName != null && (!organizationName.isBlank()) + && statusPageName != null && (!statusPageName.isBlank()); + } +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/model/user/input/UserLoginInput.java b/src/main/java/cc/pulseapp/api/model/user/input/UserLoginInput.java index 8c4cb9e..7e5ceb6 100644 --- a/src/main/java/cc/pulseapp/api/model/user/input/UserLoginInput.java +++ b/src/main/java/cc/pulseapp/api/model/user/input/UserLoginInput.java @@ -6,7 +6,7 @@ import lombok.Getter; import lombok.ToString; /** - * Input to login a {@link User}. + * The input to login a {@link User}. * * @author Braydon */ diff --git a/src/main/java/cc/pulseapp/api/model/user/input/UserRegistrationInput.java b/src/main/java/cc/pulseapp/api/model/user/input/UserRegistrationInput.java index 0be7874..5301431 100644 --- a/src/main/java/cc/pulseapp/api/model/user/input/UserRegistrationInput.java +++ b/src/main/java/cc/pulseapp/api/model/user/input/UserRegistrationInput.java @@ -6,7 +6,7 @@ import lombok.Getter; import lombok.ToString; /** - * Input to register a new {@link User}. + * The input to register a new {@link User}. * * @author Braydon */ diff --git a/src/main/java/cc/pulseapp/api/repository/OrganizationRepository.java b/src/main/java/cc/pulseapp/api/repository/OrganizationRepository.java new file mode 100644 index 0000000..d0e0e7e --- /dev/null +++ b/src/main/java/cc/pulseapp/api/repository/OrganizationRepository.java @@ -0,0 +1,20 @@ +package cc.pulseapp.api.repository; + +import cc.pulseapp.api.model.org.Organization; +import lombok.NonNull; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * The repository for interacting with {@link Organization}'s. + * + * @author Braydon + */ +public interface OrganizationRepository extends MongoRepository { + /** + * Find an organization by its name. + * + * @param name the name of the org + * @return the org with the name + */ + Organization findByNameIgnoreCase(@NonNull String name); +} \ No newline at end of file diff --git a/src/main/java/cc/pulseapp/api/service/AuthService.java b/src/main/java/cc/pulseapp/api/service/AuthService.java index 89b7156..35ce6ce 100644 --- a/src/main/java/cc/pulseapp/api/service/AuthService.java +++ b/src/main/java/cc/pulseapp/api/service/AuthService.java @@ -1,11 +1,11 @@ package cc.pulseapp.api.service; -import cc.pulseapp.api.common.EnvironmentUtils; import cc.pulseapp.api.common.HashUtils; import cc.pulseapp.api.common.RequestUtils; import cc.pulseapp.api.common.StringUtils; import cc.pulseapp.api.exception.impl.BadRequestException; import cc.pulseapp.api.exception.impl.ResourceNotFoundException; +import cc.pulseapp.api.model.Feature; import cc.pulseapp.api.model.IGenericResponse; import cc.pulseapp.api.model.user.Session; import cc.pulseapp.api.model.user.User; @@ -69,6 +69,10 @@ public final class AuthService { */ @NonNull public Session registerUser(@NonNull HttpServletRequest request, UserRegistrationInput input) throws BadRequestException { + // Ensure user registration is enabled + if (!Feature.USER_REGISTRATION_ENABLED.isEnabled()) { + throw new BadRequestException(Error.REGISTRATION_DISABLED); + } validateRegistrationInput(input); // Ensure the input is valid // Ensure the given email hasn't been used before @@ -175,7 +179,7 @@ public final class AuthService { private void validateRegistrationInput(UserRegistrationInput input) throws BadRequestException { // Ensure the input was provided if (input == null || (!input.isValid())) { - throw new BadRequestException(Error.MALFORMED_INPUT); + throw new BadRequestException(Error.MALFORMED_REGISTRATION_INPUT); } // Ensure the email is valid if (!StringUtils.isValidEmail(input.getEmail())) { @@ -207,7 +211,7 @@ public final class AuthService { private void validateLoginInput(UserLoginInput input) throws BadRequestException { // Ensure the input was provided if (input == null || (!input.isValid())) { - throw new BadRequestException(Error.MALFORMED_INPUT); + throw new BadRequestException(Error.MALFORMED_LOGIN_INPUT); } // Ensure the email is valid if (input.getEmail() != null && (!StringUtils.isValidEmail(input.getEmail()))) { @@ -217,8 +221,13 @@ public final class AuthService { captchaService.validateCaptcha(input.getCaptchaResponse()); } + /** + * Authentication errors. + */ private enum Error implements IGenericResponse { - MALFORMED_INPUT, + REGISTRATION_DISABLED, + MALFORMED_REGISTRATION_INPUT, + MALFORMED_LOGIN_INPUT, EMAIL_INVALID, USERNAME_INVALID, USER_NOT_FOUND, diff --git a/src/main/java/cc/pulseapp/api/service/UserService.java b/src/main/java/cc/pulseapp/api/service/UserService.java index 6afa0c9..dce5ac7 100644 --- a/src/main/java/cc/pulseapp/api/service/UserService.java +++ b/src/main/java/cc/pulseapp/api/service/UserService.java @@ -1,8 +1,12 @@ package cc.pulseapp.api.service; import cc.pulseapp.api.common.StringUtils; +import cc.pulseapp.api.exception.impl.BadRequestException; +import cc.pulseapp.api.model.IGenericResponse; import cc.pulseapp.api.model.user.User; import cc.pulseapp.api.model.user.UserDTO; +import cc.pulseapp.api.model.user.UserFlag; +import cc.pulseapp.api.model.user.input.CompleteOnboardingInput; import cc.pulseapp.api.repository.UserRepository; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; @@ -25,15 +29,22 @@ public final class UserService { */ @NonNull private final SnowflakeService snowflakeService; + /** + * The organization service to use. + */ + @NonNull private final OrganizationService orgService; + /** * The user repository to use. */ @NonNull private final UserRepository userRepository; @Autowired - public UserService(@NonNull AuthService authService, @NonNull SnowflakeService snowflakeService, @NonNull UserRepository userRepository) { + public UserService(@NonNull AuthService authService, @NonNull SnowflakeService snowflakeService, + @NonNull OrganizationService orgService, @NonNull UserRepository userRepository) { this.authService = authService; this.snowflakeService = snowflakeService; + this.orgService = orgService; this.userRepository = userRepository; } @@ -57,4 +68,26 @@ public final class UserService { public boolean doesUserExist(@NonNull String email) { return StringUtils.isValidEmail(email) && userRepository.findByEmailIgnoreCase(email) != null; } + + public void completeOnboarding(CompleteOnboardingInput input) { + // Ensure the input was provided + if (input == null || (!input.isValid())) { + throw new BadRequestException(Error.MALFORMED_ONBOARDING_INPUT); + } + User user = authService.getAuthenticatedUser(); + if (user.hasFlag(UserFlag.COMPLETED_ONBOARDING)) { // Already completed + throw new BadRequestException(Error.ALREADY_ONBOARDED); + } + orgService.createOrganization(input.getOrganizationName(), user); // Create the org + user.addFlag(UserFlag.COMPLETED_ONBOARDING); // Flag completed onboarding + userRepository.save(user); + } + + /** + * User errors. + */ + private enum Error implements IGenericResponse { + MALFORMED_ONBOARDING_INPUT, + ALREADY_ONBOARDED, + } } \ No newline at end of file