user registration
This commit is contained in:
parent
1de8a8df8c
commit
aa3d381dd3
@ -0,0 +1,3 @@
|
||||
# API
|
||||
|
||||
The backend API responsible for making things work!
|
63
pom.xml
63
pom.xml
@ -48,6 +48,19 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<!-- Dependency Management -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-java-bom</artifactId>
|
||||
<version>4.4.4</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<dependencies>
|
||||
<!-- Spring -->
|
||||
@ -63,14 +76,52 @@
|
||||
<version>1.18.34</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.relops</groupId>
|
||||
<artifactId>snowflake</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Unirest -->
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-java-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-modules-gson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MongoDB -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Error Reporting & Metrics -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.sentry</groupId>-->
|
||||
<!-- <artifactId>sentry-spring-boot-starter-jakarta</artifactId>-->
|
||||
<!-- <version>8.0.0-alpha.4</version>-->
|
||||
<!-- <scope>compile</scope>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
|
||||
<version>8.0.0-alpha.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.questdb</groupId>
|
||||
<artifactId>questdb</artifactId>
|
||||
|
56
src/main/java/cc/pulseapp/api/common/HashUtils.java
Normal file
56
src/main/java/cc/pulseapp/api/common/HashUtils.java
Normal file
@ -0,0 +1,56 @@
|
||||
package cc.pulseapp.api.common;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class HashUtils {
|
||||
private static final SecretKeyFactory PBKDF2;
|
||||
private static final int ITERATION_COUNT = 512000;
|
||||
private static final int KEY_LENGTH = 256;
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
static {
|
||||
try {
|
||||
PBKDF2 = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the given input.
|
||||
*
|
||||
* @param salt the salt to hash with
|
||||
* @param input the input to hash
|
||||
* @return the hashed input
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String hash(byte[] salt, @NonNull String input) {
|
||||
KeySpec spec = new PBEKeySpec(input.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH);
|
||||
byte[] hash = PBKDF2.generateSecret(spec).getEncoded();
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a salt.
|
||||
*
|
||||
* @return the generated salt
|
||||
*/
|
||||
public static byte[] generateSalt() {
|
||||
byte[] salt = new byte[32];
|
||||
RANDOM.nextBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
119
src/main/java/cc/pulseapp/api/common/StringUtils.java
Normal file
119
src/main/java/cc/pulseapp/api/common/StringUtils.java
Normal file
@ -0,0 +1,119 @@
|
||||
package cc.pulseapp.api.common;
|
||||
|
||||
import cc.pulseapp.api.model.IGenericResponse;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class StringUtils {
|
||||
private static final String ALPHABET_STRING = "abcdefghijklmnopqrstuvwxyzABCDEFGJKLMNPRSTUVWXYZ";
|
||||
private static final String NUMERIC_STRING = "0123456789";
|
||||
private static final String SPECIAL_STRING = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
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-zA-Z0-9_.]*$");
|
||||
|
||||
/**
|
||||
* Check if the given email is valid.
|
||||
*
|
||||
* @param email the email to check
|
||||
* @return whether the email is valid
|
||||
*/
|
||||
public static boolean isValidEmail(@NonNull String email) {
|
||||
return !email.isBlank() && EMAIL_PATTERN.matcher(email).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given username is valid.
|
||||
*
|
||||
* @param username the username to check
|
||||
* @return whether the username is valid
|
||||
*/
|
||||
public static boolean isValidUsername(@NonNull String username) {
|
||||
return USERNAME_PATTERN.matcher(username).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given password meets the requirements.
|
||||
*
|
||||
* @param password the password to check
|
||||
* @return the error if password requirements are not met, otherwise null
|
||||
*/
|
||||
public static PasswordError checkPasswordRequirements(@NonNull String password) {
|
||||
return checkPasswordRequirements(password, 8, 76, true, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given password meets the requirements.
|
||||
*
|
||||
* @param password the password to check
|
||||
* @param minLength the minimum length of the password
|
||||
* @param maxLength the maximum length of the password
|
||||
* @param alphabet whether the password must contain alphabet characters
|
||||
* @param numeric whether the password must contain numeric characters
|
||||
* @param special whether the password must contain special characters
|
||||
* @return the error if password requirements are not met, otherwise null
|
||||
*/
|
||||
private static PasswordError checkPasswordRequirements(@NonNull String password, int minLength, int maxLength, boolean alphabet, boolean numeric, boolean special) {
|
||||
boolean tooShort = password.length() < minLength;
|
||||
if (password.length() < minLength || password.length() > maxLength) {
|
||||
return tooShort ? PasswordError.PASSWORD_TOO_SHORT : PasswordError.PASSWORD_TOO_LONG;
|
||||
}
|
||||
if (alphabet && !password.matches(".*[a-zA-Z].*")) {
|
||||
return PasswordError.PASSWORD_MISSING_ALPHABET;
|
||||
}
|
||||
if (numeric && !password.matches(".*\\d.*")) {
|
||||
return PasswordError.PASSWORD_MISSING_NUMERIC;
|
||||
}
|
||||
if (special && !password.matches(".*[^a-zA-Z0-9].*")) {
|
||||
return PasswordError.PASSWORD_MISSING_SPECIAL;
|
||||
}
|
||||
return null; // Password meets the requirements
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string with the given length.
|
||||
*
|
||||
* @param length the length of the string
|
||||
* @param alphabet whether the string should contain alphabet characters
|
||||
* @param numeric whether the string should contain numeric characters
|
||||
* @param special whether the string should contain special characters
|
||||
* @return the generated random string
|
||||
*/
|
||||
@NonNull
|
||||
public static String generateRandom(int length, boolean alphabet, boolean numeric, boolean special) {
|
||||
if (length < 1) {
|
||||
throw new IllegalArgumentException("Length must be at least 1");
|
||||
}
|
||||
if (!alphabet && !numeric && !special) { // Validate
|
||||
throw new IllegalArgumentException("At least one of alphabet, numeric, or special must be true");
|
||||
}
|
||||
// Build the symbols string
|
||||
StringBuilder symbols = new StringBuilder();
|
||||
if (alphabet) symbols.append(ALPHABET_STRING);
|
||||
if (numeric) symbols.append(NUMERIC_STRING);
|
||||
if (special) symbols.append(SPECIAL_STRING);
|
||||
|
||||
// Generate the random string
|
||||
char[] buffer = new char[length];
|
||||
for (int idx = 0; idx < buffer.length; ++idx) {
|
||||
buffer[idx] = symbols.charAt(RANDOM.nextInt(symbols.length()));
|
||||
}
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
public enum PasswordError implements IGenericResponse {
|
||||
PASSWORD_TOO_SHORT,
|
||||
PASSWORD_TOO_LONG,
|
||||
PASSWORD_MISSING_ALPHABET,
|
||||
PASSWORD_MISSING_NUMERIC,
|
||||
PASSWORD_MISSING_SPECIAL
|
||||
}
|
||||
}
|
73
src/main/java/cc/pulseapp/api/config/RedisConfig.java
Normal file
73
src/main/java/cc/pulseapp/api/config/RedisConfig.java
Normal file
@ -0,0 +1,73 @@
|
||||
package cc.pulseapp.api.config;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Configuration
|
||||
@Log4j2(topic = "Redis")
|
||||
public class RedisConfig {
|
||||
/**
|
||||
* The Redis server host.
|
||||
*/
|
||||
@Value("${spring.data.redis.host}")
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* The Redis server port.
|
||||
*/
|
||||
@Value("${spring.data.redis.port}")
|
||||
private int port;
|
||||
|
||||
/**
|
||||
* The Redis database index.
|
||||
*/
|
||||
@Value("${spring.data.redis.database}")
|
||||
private int database;
|
||||
|
||||
/**
|
||||
* The optional Redis password.
|
||||
*/
|
||||
@Value("${spring.data.redis.auth}")
|
||||
private String auth;
|
||||
|
||||
/**
|
||||
* Build the config to use for Redis.
|
||||
*
|
||||
* @return the config
|
||||
* @see RedisTemplate for config
|
||||
*/
|
||||
@Bean @NonNull
|
||||
public RedisTemplate<String, Object> redisTemplate() {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(jedisConnectionFactory());
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the connection factory to use
|
||||
* when making connections to Redis.
|
||||
*
|
||||
* @return the built factory
|
||||
* @see JedisConnectionFactory for factory
|
||||
*/
|
||||
@Bean @NonNull
|
||||
public JedisConnectionFactory jedisConnectionFactory() {
|
||||
log.info("Connecting to Redis at {}:{}/{}", host, port, database);
|
||||
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
|
||||
config.setDatabase(database);
|
||||
if (!auth.trim().isEmpty()) { // Auth with our provided password
|
||||
log.info("Using auth...");
|
||||
config.setPassword(auth);
|
||||
}
|
||||
return new JedisConnectionFactory(config);
|
||||
}
|
||||
}
|
46
src/main/java/cc/pulseapp/api/controller/AppController.java
Normal file
46
src/main/java/cc/pulseapp/api/controller/AppController.java
Normal file
@ -0,0 +1,46 @@
|
||||
package cc.pulseapp.api.controller;
|
||||
|
||||
import cc.pulseapp.api.common.EnvironmentUtils;
|
||||
import cc.pulseapp.api.model.AppInformation;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* The root controller for this app.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public final class AppController {
|
||||
/**
|
||||
* The build properties for this app, null if not available.
|
||||
*/
|
||||
private final BuildProperties buildProperties;
|
||||
|
||||
@Autowired
|
||||
public AppController(@Nullable BuildProperties buildProperties) {
|
||||
this.buildProperties = buildProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* A GET endpoint to get info about this app.
|
||||
*
|
||||
* @return the info response
|
||||
*/
|
||||
@GetMapping @ResponseBody @NonNull
|
||||
public ResponseEntity<AppInformation> getAppInfo() {
|
||||
return ResponseEntity.ok(new AppInformation(
|
||||
buildProperties == null ? "unknown" : buildProperties.getVersion(),
|
||||
EnvironmentUtils.isProduction() ? "production" : "staging"
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package cc.pulseapp.api.controller.v1;
|
||||
|
||||
import cc.pulseapp.api.exception.impl.BadRequestException;
|
||||
import cc.pulseapp.api.model.user.AuthToken;
|
||||
import cc.pulseapp.api.model.user.input.UserRegistrationInput;
|
||||
import cc.pulseapp.api.service.AuthService;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* This controller is responsible for
|
||||
* handling user authentication requests.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/v1/auth", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public final class AuthController {
|
||||
/**
|
||||
* The user service to use.
|
||||
*/
|
||||
@NonNull private final AuthService authService;
|
||||
|
||||
@Autowired
|
||||
public AuthController(@NonNull AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@PostMapping("/register") @ResponseBody @NonNull
|
||||
public ResponseEntity<AuthToken> register(UserRegistrationInput input) throws BadRequestException {
|
||||
return ResponseEntity.ok(authService.registerUser(input));
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package cc.pulseapp.api.exception.impl;
|
||||
|
||||
import cc.pulseapp.api.model.IGenericResponse;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
@ -12,7 +13,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public final class BadRequestException extends RuntimeException {
|
||||
public BadRequestException(@NonNull String message) {
|
||||
super(message);
|
||||
public BadRequestException(@NonNull IGenericResponse error) {
|
||||
super(error.name());
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
* @author Braydon
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Slf4j(topic = "Req/Res Transaction")
|
||||
@Slf4j(topic = "HTTP Request")
|
||||
public class RequestLogger implements ResponseBodyAdvice<Object> {
|
||||
@Override
|
||||
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
|
24
src/main/java/cc/pulseapp/api/model/AppInformation.java
Normal file
24
src/main/java/cc/pulseapp/api/model/AppInformation.java
Normal file
@ -0,0 +1,24 @@
|
||||
package cc.pulseapp.api.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Information about this app.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public final class AppInformation {
|
||||
/**
|
||||
* The version of the app.
|
||||
*/
|
||||
@NonNull private final String version;
|
||||
|
||||
/**
|
||||
* The environment of the app.
|
||||
*/
|
||||
@NonNull private final String environment;
|
||||
}
|
17
src/main/java/cc/pulseapp/api/model/IGenericResponse.java
Normal file
17
src/main/java/cc/pulseapp/api/model/IGenericResponse.java
Normal file
@ -0,0 +1,17 @@
|
||||
package cc.pulseapp.api.model;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
/**
|
||||
* Represents a generic response.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface IGenericResponse {
|
||||
/**
|
||||
* Get the name of this response.
|
||||
*
|
||||
* @return the response name
|
||||
*/
|
||||
@NonNull String name();
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cc.pulseapp.api.model;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
public final class User {
|
||||
/**
|
||||
* The snowflake id of this user.
|
||||
*/
|
||||
@EqualsAndHashCode.Include private final long id;
|
||||
|
||||
/**
|
||||
* This user's username.
|
||||
*/
|
||||
@NonNull private final String username;
|
||||
|
||||
/**
|
||||
* The password for this user.
|
||||
*/
|
||||
@NonNull private final String password;
|
||||
|
||||
/**
|
||||
* The salt for this user's password.
|
||||
*/
|
||||
@NonNull private final String passwordSalt;
|
||||
|
||||
/**
|
||||
* The date this user last logged in.
|
||||
*/
|
||||
@NonNull private final Date lastLogin;
|
||||
}
|
33
src/main/java/cc/pulseapp/api/model/org/Organization.java
Normal file
33
src/main/java/cc/pulseapp/api/model/org/Organization.java
Normal file
@ -0,0 +1,33 @@
|
||||
package cc.pulseapp.api.model.org;
|
||||
|
||||
import cc.pulseapp.api.model.user.User;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
/**
|
||||
* An organization owned by a {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
@Document("organizations")
|
||||
public final class Organization {
|
||||
/**
|
||||
* The snowflake id of this organization.
|
||||
*/
|
||||
@Id @EqualsAndHashCode.Include private final long id;
|
||||
|
||||
/**
|
||||
* The name of this organization.
|
||||
*/
|
||||
@Indexed @NonNull private final String name;
|
||||
|
||||
/**
|
||||
* The snowflake of the {@link User}
|
||||
* that owns this organization.
|
||||
*/
|
||||
@Indexed private final long ownerSnowflake;
|
||||
}
|
61
src/main/java/cc/pulseapp/api/model/page/StatusPage.java
Normal file
61
src/main/java/cc/pulseapp/api/model/page/StatusPage.java
Normal file
@ -0,0 +1,61 @@
|
||||
package cc.pulseapp.api.model.page;
|
||||
|
||||
import cc.pulseapp.api.model.org.Organization;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
@Document("pages")
|
||||
public final class StatusPage {
|
||||
/**
|
||||
* The snowflake id of this status page.
|
||||
*/
|
||||
@Id @EqualsAndHashCode.Include private final long id;
|
||||
|
||||
/**
|
||||
* The name of this status page.
|
||||
*/
|
||||
@Indexed @NonNull private final String name;
|
||||
|
||||
/**
|
||||
* The description of this status page, if any.
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* The slug of this status page.
|
||||
*/
|
||||
@NonNull private final String slug;
|
||||
|
||||
/**
|
||||
* The id to the logo of this status page, if any.
|
||||
*/
|
||||
private final String logo;
|
||||
|
||||
/**
|
||||
* The id to the banner of this status page, if any.
|
||||
*/
|
||||
private final String banner;
|
||||
|
||||
/**
|
||||
* The theme of this status page.
|
||||
*/
|
||||
@NonNull private final StatusPageTheme theme;
|
||||
|
||||
/**
|
||||
* Whether this status page is visible in search engines.
|
||||
*/
|
||||
private final boolean visibleInSearchEngines;
|
||||
|
||||
/**
|
||||
* The snowflake of the {@link Organization}
|
||||
* that owns this status page.
|
||||
*/
|
||||
private final long orgSnowflake;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package cc.pulseapp.api.model.page;
|
||||
|
||||
/**
|
||||
* The theme of a {@link StatusPage}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public enum StatusPageTheme {
|
||||
/**
|
||||
* The theme is automatically chosen based on the user's OS.
|
||||
*/
|
||||
AUTO,
|
||||
|
||||
/**
|
||||
* The theme is forced to be dark.
|
||||
*/
|
||||
DARK,
|
||||
|
||||
/**
|
||||
* The theme is forced to be light.
|
||||
*/
|
||||
LIGHT
|
||||
}
|
45
src/main/java/cc/pulseapp/api/model/user/AuthToken.java
Normal file
45
src/main/java/cc/pulseapp/api/model/user/AuthToken.java
Normal file
@ -0,0 +1,45 @@
|
||||
package cc.pulseapp.api.model.user;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An authentication token for a {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
@RedisHash(value = "auth_token", timeToLive = 30 * 24 * 60 * 60) // Expire in 30 days (days, hours, mins, secs)
|
||||
public final class AuthToken {
|
||||
/**
|
||||
* The ID of this token.
|
||||
*/
|
||||
@Id @JsonIgnore @NonNull private final UUID id;
|
||||
|
||||
/**
|
||||
* The snowflake of the user this token is for.
|
||||
*/
|
||||
@JsonIgnore private final long userSnowflake;
|
||||
|
||||
/**
|
||||
* The access token for the user.
|
||||
*/
|
||||
@Indexed @NonNull private final String accessToken;
|
||||
|
||||
/**
|
||||
* The refresh token for the user.
|
||||
*/
|
||||
@Indexed @NonNull private final String refreshToken;
|
||||
|
||||
/**
|
||||
* The unix timestamp of when this token expires.
|
||||
*/
|
||||
private final long expires;
|
||||
}
|
66
src/main/java/cc/pulseapp/api/model/user/User.java
Normal file
66
src/main/java/cc/pulseapp/api/model/user/User.java
Normal file
@ -0,0 +1,66 @@
|
||||
package cc.pulseapp.api.model.user;
|
||||
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
@Document("users")
|
||||
public final class User {
|
||||
/**
|
||||
* The snowflake id of this user.
|
||||
*/
|
||||
@Id @EqualsAndHashCode.Include private final long id;
|
||||
|
||||
/**
|
||||
* This user's email.
|
||||
*/
|
||||
@Indexed @NonNull private final String email;
|
||||
|
||||
/**
|
||||
* This user's username.
|
||||
*/
|
||||
@Indexed @NonNull private final String username;
|
||||
|
||||
/**
|
||||
* The password for this user.
|
||||
*/
|
||||
@NonNull private final String password;
|
||||
|
||||
/**
|
||||
* The salt for this user's password.
|
||||
*/
|
||||
@NonNull private final String passwordSalt;
|
||||
|
||||
/**
|
||||
* The tier of this user.
|
||||
*/
|
||||
@NonNull private final UserTier tier;
|
||||
|
||||
/**
|
||||
* The flags for this user.
|
||||
*/
|
||||
private final int flags;
|
||||
|
||||
/**
|
||||
* The date this user last logged in.
|
||||
*/
|
||||
@NonNull private final Date lastLogin;
|
||||
|
||||
/**
|
||||
* Check if this user has a given flag.
|
||||
*
|
||||
* @param flag the flag to check
|
||||
* @return whether this user has the flag
|
||||
*/
|
||||
public boolean hasFlag(@NonNull UserFlag flag) {
|
||||
return (flags & flag.ordinal()) != 0;
|
||||
}
|
||||
}
|
64
src/main/java/cc/pulseapp/api/model/user/UserDTO.java
Normal file
64
src/main/java/cc/pulseapp/api/model/user/UserDTO.java
Normal file
@ -0,0 +1,64 @@
|
||||
package cc.pulseapp.api.model.user;
|
||||
|
||||
import lombok.*;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* The DTO for a {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
|
||||
public final class UserDTO {
|
||||
/**
|
||||
* The snowflake id of this user.
|
||||
*/
|
||||
@EqualsAndHashCode.Include private final long id;
|
||||
|
||||
/**
|
||||
* This user's email.
|
||||
*/
|
||||
@Indexed @NonNull private final String email;
|
||||
|
||||
/**
|
||||
* This user's username.
|
||||
*/
|
||||
@Indexed @NonNull private final String username;
|
||||
|
||||
/**
|
||||
* The tier of this user.
|
||||
*/
|
||||
@NonNull private final UserTier tier;
|
||||
|
||||
/**
|
||||
* The date this user last logged in.
|
||||
*/
|
||||
@NonNull private final Date lastLogin;
|
||||
|
||||
/**
|
||||
* The flags for this user.
|
||||
*/
|
||||
private final int flags;
|
||||
|
||||
/**
|
||||
* The date this user was created.
|
||||
*/
|
||||
@NonNull private final Date created;
|
||||
|
||||
/**
|
||||
* Create a DTO from the given user.
|
||||
*
|
||||
* @param user the user
|
||||
* @param creationTime the user's creation time
|
||||
* @return the user dto
|
||||
*/
|
||||
@NonNull
|
||||
public static UserDTO asDTO(@NonNull User user, @NonNull Date creationTime) {
|
||||
return new UserDTO(user.getId(), user.getEmail(), user.getUsername(), user.getTier(),
|
||||
user.getLastLogin(), user.getFlags(), creationTime
|
||||
);
|
||||
}
|
||||
}
|
18
src/main/java/cc/pulseapp/api/model/user/UserFlag.java
Normal file
18
src/main/java/cc/pulseapp/api/model/user/UserFlag.java
Normal file
@ -0,0 +1,18 @@
|
||||
package cc.pulseapp.api.model.user;
|
||||
|
||||
/**
|
||||
* Flags for a {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public enum UserFlag {
|
||||
/**
|
||||
* The user is disabled.
|
||||
*/
|
||||
DISABLED,
|
||||
|
||||
/**
|
||||
* The user completed the onboarding process.
|
||||
*/
|
||||
COMPLETED_ONBOARDING
|
||||
}
|
35
src/main/java/cc/pulseapp/api/model/user/UserTier.java
Normal file
35
src/main/java/cc/pulseapp/api/model/user/UserTier.java
Normal file
@ -0,0 +1,35 @@
|
||||
package cc.pulseapp.api.model.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* The tier of a {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public enum UserTier {
|
||||
FREE(1, 2, false, 10);
|
||||
|
||||
/**
|
||||
* The maximum number of organizations a user can have.
|
||||
*/
|
||||
private final int maxOrganizations;
|
||||
|
||||
/**
|
||||
* The maximum number of status pages a user can have.
|
||||
*/
|
||||
private final int maxStatusPages;
|
||||
|
||||
/**
|
||||
* Whether this tier has banners on status pages.
|
||||
*/
|
||||
private final boolean statusPageBanners;
|
||||
|
||||
/**
|
||||
* The maximum number of components a user can have on a status page.
|
||||
*/
|
||||
private final int maxStatusPageComponents;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package cc.pulseapp.api.model.user.input;
|
||||
|
||||
import cc.pulseapp.api.model.user.User;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Input to register a new {@link User}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public final class UserRegistrationInput {
|
||||
/**
|
||||
* The name of the user to create.
|
||||
*/
|
||||
private final String email;
|
||||
|
||||
/**
|
||||
* The username of the user to create.
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* The password of the user to create.
|
||||
*/
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* The confirmation password of the user to create.
|
||||
*/
|
||||
private final String passwordConfirmation;
|
||||
|
||||
/**
|
||||
* The captcha response token to validate.
|
||||
*/
|
||||
private final String captchaResponse;
|
||||
|
||||
/**
|
||||
* Check if this input is valid.
|
||||
*
|
||||
* @return whether this input is valid
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return email != null && (!email.isBlank())
|
||||
&& password != null && (!password.isBlank())
|
||||
&& passwordConfirmation != null && (!passwordConfirmation.isBlank())
|
||||
&& captchaResponse != null && (!captchaResponse.isBlank());
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package cc.pulseapp.api.repository;
|
||||
|
||||
import cc.pulseapp.api.model.user.AuthToken;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* The repository for {@link AuthToken}'s.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface AuthTokenRepository extends CrudRepository<AuthToken, String> {
|
||||
/**
|
||||
* Find an auth token by the access token.
|
||||
*
|
||||
* @param accessToken the access token to search by
|
||||
* @return the auth token, null if none
|
||||
*/
|
||||
AuthToken findByAccessToken(@NonNull String accessToken);
|
||||
}
|
20
src/main/java/cc/pulseapp/api/repository/UserRepository.java
Normal file
20
src/main/java/cc/pulseapp/api/repository/UserRepository.java
Normal file
@ -0,0 +1,20 @@
|
||||
package cc.pulseapp.api.repository;
|
||||
|
||||
import cc.pulseapp.api.model.user.User;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
/**
|
||||
* The repository for interacting with {@link User}'s.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface UserRepository extends MongoRepository<User, Long> {
|
||||
/**
|
||||
* Find a user by their email.
|
||||
*
|
||||
* @param email the email of the user
|
||||
* @return the user with the email
|
||||
*/
|
||||
User findByEmailIgnoreCase(@NonNull String email);
|
||||
}
|
147
src/main/java/cc/pulseapp/api/service/AuthService.java
Normal file
147
src/main/java/cc/pulseapp/api/service/AuthService.java
Normal file
@ -0,0 +1,147 @@
|
||||
package cc.pulseapp.api.service;
|
||||
|
||||
import cc.pulseapp.api.common.EnvironmentUtils;
|
||||
import cc.pulseapp.api.common.HashUtils;
|
||||
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.AuthToken;
|
||||
import cc.pulseapp.api.model.user.User;
|
||||
import cc.pulseapp.api.model.user.UserFlag;
|
||||
import cc.pulseapp.api.model.user.UserTier;
|
||||
import cc.pulseapp.api.model.user.input.UserRegistrationInput;
|
||||
import cc.pulseapp.api.repository.AuthTokenRepository;
|
||||
import cc.pulseapp.api.repository.UserRepository;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Service
|
||||
public final class AuthService {
|
||||
/**
|
||||
* The service to use for captcha validation.
|
||||
*/
|
||||
@NonNull private final CaptchaService captchaService;
|
||||
|
||||
/**
|
||||
* The service to use for snowflake generation and timestamp extraction.
|
||||
*/
|
||||
@NonNull private final SnowflakeService snowflakeService;
|
||||
|
||||
/**
|
||||
* The repository to store and retrieve users.
|
||||
*/
|
||||
@NonNull private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* The auth token repository for generating and validating auth tokens.
|
||||
*/
|
||||
@NonNull private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
@Autowired
|
||||
public AuthService(@NonNull CaptchaService captchaService, @NonNull SnowflakeService snowflakeService,
|
||||
@NonNull UserRepository userRepository, @NonNull AuthTokenRepository authTokenRepository) {
|
||||
this.captchaService = captchaService;
|
||||
this.snowflakeService = snowflakeService;
|
||||
this.userRepository = userRepository;
|
||||
this.authTokenRepository = authTokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
*
|
||||
* @param input the registration input
|
||||
* @return the registered user's auth token
|
||||
* @throws BadRequestException if the input has an error
|
||||
*/
|
||||
@NonNull
|
||||
public AuthToken registerUser(UserRegistrationInput input) throws BadRequestException {
|
||||
validateRegistrationInput(input); // Ensure the input is valid
|
||||
|
||||
// Ensure the given email hasn't been used before
|
||||
if (userRepository.findByEmailIgnoreCase(input.getEmail()) != null) {
|
||||
throw new BadRequestException(Error.EMAIL_ALREADY_USED);
|
||||
}
|
||||
|
||||
// Create the user and return it
|
||||
byte[] salt = HashUtils.generateSalt();
|
||||
Date now = new Date();
|
||||
return generateAuthToken(userRepository.save(new User(
|
||||
snowflakeService.generateSnowflake(), input.getEmail(), input.getUsername(),
|
||||
HashUtils.hash(salt, input.getPassword()), Base64.getEncoder().encodeToString(salt),
|
||||
UserTier.FREE, 0, now
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an auth token for a user.
|
||||
*
|
||||
* @param user the user to generate for
|
||||
* @return the generated auth token
|
||||
* @throws BadRequestException if the user is disabled
|
||||
*/
|
||||
@NonNull
|
||||
private AuthToken generateAuthToken(@NonNull User user) throws BadRequestException {
|
||||
// User's account has been disabled
|
||||
if (user.hasFlag(UserFlag.DISABLED)) {
|
||||
throw new BadRequestException(Error.USER_DISABLED);
|
||||
}
|
||||
return authTokenRepository.save(new AuthToken(
|
||||
UUID.randomUUID(), user.getId(),
|
||||
StringUtils.generateRandom(128, true, true, false),
|
||||
StringUtils.generateRandom(128, true, true, false),
|
||||
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30L)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given registration input.
|
||||
*
|
||||
* @param input the registration input
|
||||
* @throws BadRequestException if the input has an error
|
||||
*/
|
||||
private void validateRegistrationInput(UserRegistrationInput input) throws BadRequestException {
|
||||
// Ensure the input was provided
|
||||
if (input == null || (!input.isValid())) {
|
||||
throw new BadRequestException(Error.MALFORMED_INPUT);
|
||||
}
|
||||
// Ensure the email is valid
|
||||
if (!StringUtils.isValidEmail(input.getEmail())) {
|
||||
throw new BadRequestException(Error.EMAIL_INVALID);
|
||||
}
|
||||
// Ensure the username is valid
|
||||
if (!StringUtils.isValidUsername(input.getUsername())) {
|
||||
throw new BadRequestException(Error.USERNAME_INVALID);
|
||||
}
|
||||
// Password and confirmed password must match
|
||||
if (!input.getPassword().equals(input.getPasswordConfirmation())) {
|
||||
throw new BadRequestException(Error.PASSWORDS_DO_NOT_MATCH);
|
||||
}
|
||||
// The password must meet the requirements
|
||||
StringUtils.PasswordError passwordError = StringUtils.checkPasswordRequirements(input.getPassword());
|
||||
if (passwordError != null) {
|
||||
throw new BadRequestException(passwordError);
|
||||
}
|
||||
// Finally validate the captcha
|
||||
if (EnvironmentUtils.isProduction()) {
|
||||
captchaService.validateCaptcha(input.getCaptchaResponse());
|
||||
}
|
||||
}
|
||||
|
||||
private enum Error implements IGenericResponse {
|
||||
MALFORMED_INPUT,
|
||||
EMAIL_INVALID,
|
||||
USERNAME_INVALID,
|
||||
PASSWORDS_DO_NOT_MATCH,
|
||||
EMAIL_ALREADY_USED,
|
||||
USER_DISABLED
|
||||
}
|
||||
}
|
40
src/main/java/cc/pulseapp/api/service/CaptchaService.java
Normal file
40
src/main/java/cc/pulseapp/api/service/CaptchaService.java
Normal file
@ -0,0 +1,40 @@
|
||||
package cc.pulseapp.api.service;
|
||||
|
||||
import cc.pulseapp.api.exception.impl.BadRequestException;
|
||||
import cc.pulseapp.api.model.IGenericResponse;
|
||||
import com.google.gson.JsonObject;
|
||||
import kong.unirest.core.HttpResponse;
|
||||
import kong.unirest.core.JsonNode;
|
||||
import kong.unirest.core.Unirest;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Service
|
||||
public final class CaptchaService {
|
||||
@Value("${captcha.secret}")
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* Validates the captcha response.
|
||||
*
|
||||
* @param captchaResponse the response to validate
|
||||
* @throws BadRequestException if the response is invalid
|
||||
*/
|
||||
public void validateCaptcha(@NonNull String captchaResponse) throws BadRequestException {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("secret", secretKey);
|
||||
body.addProperty("response", captchaResponse);
|
||||
HttpResponse<JsonNode> response = Unirest.post("https://challenges.cloudflare.com/turnstile/v0/siteverify").body(body).asJson();
|
||||
if (!response.getBody().getObject().getBoolean("success")) {
|
||||
throw new BadRequestException(Error.CAPTCHA_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Error implements IGenericResponse {
|
||||
CAPTCHA_INVALID
|
||||
}
|
||||
}
|
48
src/main/java/cc/pulseapp/api/service/SnowflakeService.java
Normal file
48
src/main/java/cc/pulseapp/api/service/SnowflakeService.java
Normal file
@ -0,0 +1,48 @@
|
||||
package cc.pulseapp.api.service;
|
||||
|
||||
import com.relops.snowflake.Snowflake;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* The service responsible for generating snowflakes!
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Service
|
||||
public final class SnowflakeService {
|
||||
private static final long TIMESTAMP_SHIFT = Snowflake.NODE_SHIFT + Snowflake.SEQ_SHIFT;
|
||||
|
||||
/**
|
||||
* The snowflake instance to use.
|
||||
*/
|
||||
@NonNull private final Snowflake snowflake;
|
||||
|
||||
private SnowflakeService() {
|
||||
snowflake = new Snowflake(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new snowflake.
|
||||
*
|
||||
* @return the generated snowflake
|
||||
*/
|
||||
public long generateSnowflake() {
|
||||
return snowflake.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the creation time of the given
|
||||
* snowflake by reversing the snowflake algorithm.
|
||||
* <p>
|
||||
* The snowflake is right-shifted by 22
|
||||
* bits to get the original timestamp.
|
||||
* </p>
|
||||
*
|
||||
* @param snowflake the snowflake
|
||||
* @return the snowflake unix creation time
|
||||
*/
|
||||
public long extractCreationTime(long snowflake) {
|
||||
return snowflake >>> TIMESTAMP_SHIFT;
|
||||
}
|
||||
}
|
@ -8,19 +8,33 @@ logging:
|
||||
file:
|
||||
path: "./logs"
|
||||
|
||||
# Sentry Configuration
|
||||
sentry:
|
||||
dsn: "CHANGE_ME"
|
||||
tracesSampleRate: 1.0
|
||||
environment: "development"
|
||||
# Cloudflare Captcha Configuration
|
||||
captcha:
|
||||
secret: "CHANGE_ME"
|
||||
|
||||
# QuestDB Configuration (Metrics)
|
||||
questdb:
|
||||
enabled: false
|
||||
uri: "http::addr=localhost:9000;username=tether;password=p4$$w0rd;auto_flush_interval=5000;"
|
||||
|
||||
# Sentry Configuration
|
||||
sentry:
|
||||
dsn: "" # Leave blank for no sentry
|
||||
tracesSampleRate: 1.0
|
||||
environment: "development"
|
||||
|
||||
# Spring Configuration
|
||||
spring:
|
||||
data:
|
||||
# MongoDB Configuration
|
||||
mongodb:
|
||||
uri: "mongodb://pulseapp:p4$$w0rd@localhost:27017/pulseapp?authSource=admin"
|
||||
auto-index-creation: true # Automatically create collection indexes
|
||||
|
||||
# Don't serialize null values by default with Jackson
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
|
||||
# Ignore
|
||||
banner:
|
||||
location: "classpath:banner.txt"
|
Loading…
Reference in New Issue
Block a user