onboarding backend

This commit is contained in:
Braydon 2024-09-18 00:07:34 -04:00
parent 755ee7b2ea
commit 1bd167d0ec
9 changed files with 125 additions and 8 deletions

@ -86,6 +86,12 @@
<version>1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.flagsmith</groupId>
<artifactId>flagsmith-java-client</artifactId>
<version>7.2.0</version>
<scope>compile</scope>
</dependency>
<!-- Unirest -->
<dependency>

@ -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<Map<String, Object>> doesUserExist(@RequestParam @NonNull String email) {
return ResponseEntity.ok(Map.of("exists", userService.doesUserExist(email)));
}
@PostMapping("/complete-onboarding") @ResponseBody @NonNull
public ResponseEntity<Map<String, Object>> completeOnboarding(CompleteOnboardingInput input) throws BadRequestException {
userService.completeOnboarding(input);
return ResponseEntity.ok(Map.of("success", true));
}
}

@ -14,5 +14,10 @@ public enum UserFlag {
/**
* The user completed the onboarding process.
*/
COMPLETED_ONBOARDING
COMPLETED_ONBOARDING,
/**
* The user is an administrator.
*/
ADMINISTRATOR
}

@ -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());
}
}

@ -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
*/

@ -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
*/

@ -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<Organization, Long> {
/**
* Find an organization by its name.
*
* @param name the name of the org
* @return the org with the name
*/
Organization findByNameIgnoreCase(@NonNull String name);
}

@ -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,

@ -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,
}
}