Authorization working

This commit is contained in:
2025-02-22 16:54:37 -05:00
parent 96419f0077
commit 02c615ee0c
47 changed files with 1894 additions and 0 deletions

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"configurations": [
{
"type": "java",
"name": "Spring Boot-RaidBuilderAPI<raid-builder-api>",
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "com.mattrixwv.raidbuilder.RaidBuilderAPI",
"projectName": "raid-builder-api",
"args": "--spring.config.additional-location=local/application.properties --logging.config=local/log4j2-spring.xml",
"envFile": "${workspaceFolder}/.env"
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"cSpell.words": [
"raidbuilder",
"springframework"
]
}

View File

@@ -0,0 +1,25 @@
java.lang.reflect.Method
org.apache.cataline
org.springframework.aop
org.springframework.security
org.springframework.transaction
org.springframework.web
sun.reflect
net.sf.cglib
ByCGLIB
//Thsea re ones I'm removing because I'm not seeing anythign important in them
BySpringCGLIB //Spring impl of CGLIB?
org.springframework.cglib
org.springframework.orm //Spring automagic
org.springframework.dao //Spring automagic
org.springframework.session //Spring automagic
org.springframework.repository //Spring automagic
org.springframework.data //Spring automagic
org.springframework.scheduling //Spring automagic scheduling
org.springframework.beans //Spring automagic beans
org.springframework.boot //Spring Boot automagic
jdk.internal.reflect //Java automagic
sun.proxy //Java automagic
io.netty.util.concurrent //Server framework threading
com.google.common.util.concurrent //Threading
java.util.concurrent //Threading

4
db/1.0.0/1. schema.sql Normal file
View File

@@ -0,0 +1,4 @@
CREATE USER raid_builder WITH PASSWORD 'raid_builder';
CREATE SCHEMA raid_builder;
GRANT ALL ON SCHEMA raid_builder TO raid_builder;

View File

@@ -0,0 +1,16 @@
CREATE TYPE raid_builder.account_status AS ENUM ( 'ACTIVE', 'LOCKED', 'INACTIVE', 'DELETED', 'UNCONFIRMED');
CREATE TABLE IF NOT EXISTS raid_builder.account(
account_id uuid PRIMARY KEY,
username text UNIQUE NOT NULL,
password text NOT NULL,
login_date timestamptz,
email text UNIQUE NOT NULL,
force_reset boolean NOT NULL,
refresh_token uuid UNIQUE,
refresh_token_expiration timestamptz,
account_status raid_builder.account_status NOT NULL
);
GRANT ALL ON TABLE raid_builder.account TO raid_builder;

View File

@@ -0,0 +1,10 @@
CREATE TYPE raid_builder.account_permission_type AS ENUM ( 'ADMIN', 'USER' );
CREATE TABLE IF NOT EXISTS raid_builder.account_permission(
account_permission_id uuid PRIMARY KEY,
account_id uuid REFERENCES raid_builder.account(account_id) NOT NULL,
account_permission_type raid_builder.account_permission_type NOT NULL
);
GRANT ALL ON TABLE raid_builder.account_permission TO raid_builder;

View File

@@ -0,0 +1,14 @@
CREATE TYPE raid_builder.tutorial_status AS ENUM ( 'COMPLETED', 'NOT_COMPLETED' );
CREATE TABLE IF NOT EXISTS raid_builder.account_tutorial_status (
account_tutorial_status_id uuid PRIMARY KEY,
account_id uuid REFERENCES raid_builder.account(account_id),
games_tutorial_status raid_builder.tutorial_status NOT NULL,
game_tutorial_status raid_builder.tutorial_status NOT NULL,
raid_groups_tutorial_status raid_builder.tutorial_status NOT NULL,
raid_group_tutorial_status raid_builder.tutorial_status NOT NULL,
instance_tutorial_status raid_builder.tutorial_status NOT NULL
);
GRANT ALL ON TABLE raid_builder.account_tutorial_status TO raid_builder;

144
pom.xml Normal file
View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mattrixwv.raidbuilder</groupId>
<artifactId>raid-builder-api</artifactId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<name>Raid Builder API</name>
<url>https://api.raidbuilder.mattrixwv.com</url>
<properties>
<!--Compile-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<java.version>21</java.version>
<!--Sonarqube-->
<sonar.java.source>21</sonar.java.source>
<sonar.dependencyCheck.jsonReportPath>target/dependency-check-report.json</sonar.dependencyCheck.jsonReportPath>
<sonar.dependencyCheck.htmlReportPath>target/dependency-check-report.html</sonar.dependencyCheck.htmlReportPath>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
</parent>
<dependencies>
<!--! Spring Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--! Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.5</version>
</dependency>
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-63</artifactId>
<version>3.9.2</version>
</dependency>
<!--! Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate6</artifactId>
<version>2.18.2</version>
</dependency>
<!--! Boilerplate Generator -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<!--! Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-layout-template-json</artifactId>
<version>2.24.3</version>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,17 @@
package com.mattrixwv.raidbuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.mattrixwv.raidbuilder.config.RsaKeyProperties;
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class RaidBuilderAPI{
public static void main(String[] args){
SpringApplication.run(RaidBuilderAPI.class, args);
}
}

View File

@@ -0,0 +1,16 @@
package com.mattrixwv.raidbuilder.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountAuthorization{
public AccountPermissionType[] permissions();
}

View File

@@ -0,0 +1,94 @@
package com.mattrixwv.raidbuilder.aspect;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import com.mattrixwv.raidbuilder.annotation.AccountAuthorization;
import com.mattrixwv.raidbuilder.entity.Account;
import com.mattrixwv.raidbuilder.entity.AccountPermission;
import com.mattrixwv.raidbuilder.exception.MissingAuthorizationException;
import com.mattrixwv.raidbuilder.service.AccountPermissionService;
import com.mattrixwv.raidbuilder.service.AccountService;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Configuration
@RequiredArgsConstructor
public class AccountAuthorizationAspect{
private final AccountService accountService;
private final AccountPermissionService accountPermissionService;
@Pointcut("@annotation(com.mattrixwv.raidbuilder.annotation.AccountAuthorization)")
public void accountAuthorizationAnnotation(){
//Intentionally blank
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void mappedFunction(){
//Intentionally blank
}
@Before("accountAuthorizationAnnotation()")
public void authorizeAccount(JoinPoint joinPoint){
log.debug("Authorizing account");
//Get the annotation
AccountAuthorization accountAuthorization = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(AccountAuthorization.class);
log.debug("Required authorizations = {}", accountAuthorization);
//Return if there are no required permissions
if(accountAuthorization.permissions().length == 0){
log.debug("No required permissions");
return;
}
//Get the account
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = ((Jwt)auth.getPrincipal()).getClaimAsString("sub");
Account account = accountService.getByUsername(username);
if(account.getAccountStatus() != AccountStatus.ACTIVE){
throw new AuthorizationDeniedException("Account is not active", () -> false);
}
List<AccountPermission> accountPermissions = accountPermissionService.getByAccountId(account.getAccountId());
//Return if the account has a matching permissions
for(AccountPermission permission : accountPermissions){
for(AccountPermissionType permissionType : accountAuthorization.permissions()){
if(permission.getAccountPermissionType() == permissionType){
log.debug("User is authorized");
return;
}
}
}
log.debug("User is not authorized");
//If the user doesn't have a matching permission, throw an authorization exception
throw new AuthorizationDeniedException("User is not authorized to perform this action", () -> false);
}
@Before("mappedFunction() && !accountAuthorizationAnnotation()")
public void missingAuthorization(){
throw new MissingAuthorizationException();
}
}

View File

@@ -0,0 +1,12 @@
package com.mattrixwv.raidbuilder.config;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey){
}

View File

@@ -0,0 +1,72 @@
package com.mattrixwv.raidbuilder.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{
private final RsaKeyProperties rsaKeys;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/auth/refresh").permitAll() //Permit refresh tokens
.requestMatchers(HttpMethod.POST, "/auth/signup", "/auth/confirm").permitAll() //Permit signup operations
.requestMatchers("/auth/forgot", "/auth/forgot/*").permitAll() //Permit forgot password operations
.anyRequest().authenticated();
})
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public JwtEncoder jwtEncoder(){
JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
public JwtDecoder jwtDecoder(){
return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
}
}

View File

@@ -0,0 +1,50 @@
package com.mattrixwv.raidbuilder.config;
import java.time.Duration;
import java.time.Instant;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import com.mattrixwv.raidbuilder.entity.Account;
import com.mattrixwv.raidbuilder.service.AccountPermissionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class TokenService{
private final JwtEncoder encoder;
private final AccountPermissionService accountPermissionService;
//Fields
@Value("${jwt.accessTokenDuration}")
private Duration accessTokenDuration;
public String generateAccessToken(Account account){
log.debug("Generating access token for account {}", account.getAccountId());
String scope = accountPermissionService.getByAccountId(account.getAccountId()).stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" "));
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(accessTokenDuration))
.subject(account.getUsername())
.claim("scope", scope)
.build();
return encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}

View File

@@ -0,0 +1,60 @@
package com.mattrixwv.raidbuilder.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module;
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module.Feature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Value("${allowedOrigins}")
private String allowedOrigins;
@Override
public void addCorsMappings(@NonNull CorsRegistry registry){
log.debug("Adding CORS mappings: {}", allowedOrigins);
registry.addMapping("/**")
.allowedOriginPatterns(allowedOrigins)
.allowedMethods("GET", "PUT", "DELETE", "OPTIONS", "PATCH", "POST");
}
@Bean
public ObjectMapper objectMapper(){
ObjectMapper mapper = new ObjectMapper();
log.debug("Starting mapping configuration");
//Make sure Jackson doesn't attempt lazy loading
Hibernate6Module hibernate6Module = new Hibernate6Module();
hibernate6Module.configure(Feature.FORCE_LAZY_LOADING, false);
hibernate6Module.configure(Feature.USE_TRANSIENT_ANNOTATION, false);
hibernate6Module.configure(Feature.REQUIRE_EXPLICIT_LAZY_LOADING_MARKER, true);
hibernate6Module.configure(Feature.WRITE_MISSING_ENTITIES_AS_NULL, false);
mapper.registerModule(hibernate6Module);
//Print dates as strings
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.findAndRegisterModules();
log.debug("Completed mapping configuration");
return mapper;
}
}

View File

@@ -0,0 +1,74 @@
package com.mattrixwv.raidbuilder.config;
import java.io.IOException;
import java.util.StringJoiner;
import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class WebFilter extends OncePerRequestFilter{
@Override
public void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException{
if(!request.getMethod().equalsIgnoreCase("OPTIONS")){
setupMDC(request);
}
//Continue to the controller
filterChain.doFilter(request, response);
//Clear the MDC for the next request
MDC.clear();
}
private void setupMDC(HttpServletRequest request){
//Get the requestId
String requestId = request.getHeader("X-Request-Id");
if(requestId != null){
MDC.put("requestId", requestId);
}
//Get IP address
String forwardedFor = request.getHeader("X-Forwarded-For");
if(forwardedFor != null){
MDC.put("ip", forwardedFor.split(",")[0]);
}
//Get all of the parameters in the request and print them in the log
StringJoiner parameters = new StringJoiner(", ");
request.getParameterMap().entrySet().forEach(entry -> {
if(!entry.getKey().equals("_")){
String key = entry.getKey();
String value = "";
if(entry.getValue().length > 1){
StringJoiner joiner = new StringJoiner(", ", "[", "]");
for(String str : entry.getValue()){
joiner.add(str);
}
value = joiner.toString();
}
else{
value = entry.getValue()[0];
}
parameters.add(key + "->" + value);
}
});
if(parameters.length() > 0){
log.info("Request parameters: {}", parameters);
}
//Get the path
MDC.put("url", request.getRequestURI());
}
}

View File

@@ -0,0 +1,348 @@
package com.mattrixwv.raidbuilder.controller;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mattrixwv.raidbuilder.annotation.AccountAuthorization;
import com.mattrixwv.raidbuilder.config.TokenService;
import com.mattrixwv.raidbuilder.entity.Account;
import com.mattrixwv.raidbuilder.service.AccountService;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController{
private final ObjectMapper mapper;
private final TokenService tokenService;
private final AccountService accountService;
@PostMapping("/token")
@AccountAuthorization(permissions = {})
public ObjectNode token(Authentication authentication){
log.info("Token requested for user {}", authentication.getName());
Account account = accountService.getByUsername(authentication.getName());
String token = tokenService.generateAccessToken(account);
log.debug("Token granted {}", token);
ObjectNode tokenNode = mapper.valueToTree(account);
tokenNode.put("token", token);
return tokenNode;
}
@PostMapping("/refresh")
@AccountAuthorization(permissions = {})
public ObjectNode refresh(@RequestBody ObjectNode refreshTokenNode){
log.info("Refreshing token");
UUID refreshToken = UUID.fromString(refreshTokenNode.get("refreshToken").asText());
log.debug("refreshToken: {}", refreshToken);
Account account = accountService.getByRefreshToken(refreshToken);
if(account == null){
throw new AuthorizationDeniedException("Refresh token is invalid", () -> false);
}
//Update login date
account.setLoginDate(ZonedDateTime.now());
account = accountService.updateAccount(account);
String token = tokenService.generateAccessToken(account);
log.debug("new token: {}", token);
ObjectNode tokenNode = mapper.valueToTree(account);
tokenNode.put("token", token);
return tokenNode;
}
@PostMapping("/signup")
@AccountAuthorization(permissions = {})
public ObjectNode signup(@RequestBody Account account){
log.info("Creating account {}", account.getUsername());
ObjectNode returnNode = mapper.createObjectNode();
//Verify account object
List<String> errors = verifyNewAccount(account);
if(errors.isEmpty()){
//Create the account
account = accountService.createAccount(account);
returnNode.put("accountId", account.getAccountId().toString());
returnNode.put("status", "success");
//TODO: Send email
log.info("Successfully created account: {}", account.getAccountId());
}
else{
ArrayNode errorsNode = mapper.createArrayNode();
errors.forEach(errorsNode::add);
returnNode.set("errors", errorsNode);
log.info("Error creating account: {}", errors);
}
return returnNode;
}
@PostMapping("/confirm")
@AccountAuthorization(permissions = {})
public ObjectNode confirm(@RequestBody ObjectNode confirmNode){
UUID confirmToken = UUID.fromString(confirmNode.get("confirmToken").asText());
log.info("Confirming account with token {}", confirmToken);
ObjectNode returnNode = mapper.createObjectNode();
//Verify token
Account account = accountService.getByRefreshToken(confirmToken);
log.debug("Found account: {}", account);
if((account != null) && (account.getAccountStatus() == AccountStatus.UNCONFIRMED) && (account.getRefreshTokenExpiration().isAfter(ZonedDateTime.now()))){
accountService.confirmAccount(account);
returnNode.put("status", "success");
}
else{
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add("Account is not unconfirmed");
returnNode.set("errors", errorsNode);
}
return returnNode;
}
@PostMapping("/forgot")
@AccountAuthorization(permissions = {})
public ObjectNode forgot(@RequestParam("loginId") String loginId){
log.info("Setting up user that forgot their password: {}", loginId);
ObjectNode returnNode = mapper.createObjectNode();
//Verify the account exists
Account account = accountService.getByUsername(loginId);
if(account != null){
//Setup token
UUID token = UUID.randomUUID();
account.setRefreshToken(token);
account.setRefreshTokenExpiration(ZonedDateTime.now().plusHours(1));
account.setAccountStatus(AccountStatus.LOCKED);
account = accountService.updateAccount(account);
//TODO: Send email
returnNode.put("status", "success");
}
else{
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add("Could not find account with login " + loginId);
returnNode.set("errors", errorsNode);
}
return returnNode;
}
@PostMapping("/forgot/reset")
@AccountAuthorization(permissions = {})
public ObjectNode setNewPasswordForgot(@RequestBody ObjectNode forgotNode){
UUID forgotToken = UUID.fromString(forgotNode.get("forgotToken").asText());
String newPassword = forgotNode.get("password").asText();
log.info("Confirming user reset password (forget) with token {}", forgotToken);
ObjectNode returnNode = mapper.createObjectNode();
//Verify the account exists
Account existingAccount = accountService.getByRefreshToken(forgotToken);
if(existingAccount != null){
existingAccount = accountService.updatePassword(existingAccount.getAccountId(), newPassword);
existingAccount.setRefreshToken(null);
existingAccount.setRefreshTokenExpiration(null);
existingAccount.setAccountStatus(AccountStatus.ACTIVE);
existingAccount = accountService.updateAccount(existingAccount);
returnNode.put("status", "success");
}
else{
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add("Invalid token");
returnNode.set("errors", errorsNode);
}
return returnNode;
}
@PostMapping("/resetPassword")
@AccountAuthorization(permissions = {})
public ObjectNode resetPassword(Authentication authentication, @RequestBody ObjectNode requestNode){
log.info("Resetting password for {}", authentication.getName());
if((requestNode == null) || (!requestNode.has("password"))){
throw new IllegalArgumentException("Invalid request");
}
Account account = accountService.getByUsername(authentication.getName());
accountService.updatePassword(account.getAccountId(), requestNode.get("password").asText());
account.setForceReset(false);
accountService.updateAccount(account);
ObjectNode returnNode = mapper.createObjectNode();
returnNode.put("status", "success");
return returnNode;
}
@PostMapping("/logout")
@AccountAuthorization(permissions = {})
public ObjectNode logout(Authentication authentication){
log.info("Logging out account {}", authentication.getName());
Account account = accountService.getByUsername(authentication.getName());
if(account != null){
account.setRefreshToken(null);
account.setRefreshTokenExpiration(null);
account = accountService.updateAccount(account);
}
ObjectNode returnNode = mapper.createObjectNode();
returnNode.put("status", "success");
return returnNode;
}
@PutMapping("/{accountId}/revokeRefreshToken")
@AccountAuthorization(permissions = {AccountPermissionType.ADMIN})
public ObjectNode revokeRefreshToken(@PathVariable("accountId") UUID accountId){
log.info("Revoking refresh token for account {}", accountId);
Account account = accountService.getByAccountId(accountId);
account.setRefreshToken(null);
account.setRefreshTokenExpiration(null);
account = accountService.updateAccount(account);
ObjectNode returnNode = mapper.createObjectNode();
returnNode.put("status", "success");
returnNode.put("accountId", account.getAccountId().toString());
return returnNode;
}
private List<String> verifyNewAccount(Account account){
ArrayList<String> errors = new ArrayList<>();
//Check ID
if(account.getAccountId() != null){
errors.add("Invalid Account ID");
}
//Check login exists in entity
if((account.getUsername() == null) || (account.getUsername().isEmpty())){
errors.add("Invalid Login ID");
}
else{
//Check login doesn't exist in db
Account existingAccount = accountService.getByUsername(account.getUsername());
if(existingAccount != null){
errors.add("Login ID already exists");
}
}
//Check password exists in entity
if((account.getPassword() == null) || (account.getPassword().isEmpty())){
errors.add("No password found");
}
//Check email exists in entity
if((account.getEmail() == null) || (account.getEmail().isEmpty())){
errors.add("Invalid email");
}
else{
//Check email doesn't exist in db
Account existingAccount = accountService.getByEmail(account.getEmail());
if(existingAccount != null){
errors.add("Account with email already exists");
}
}
return errors;
}
private List<String> verifyUpdatedAccount(Account account){
ArrayList<String> errors = new ArrayList<>();
//Check ID
if(account.getAccountId() == null){
errors.add("Invalid Account ID");
}
//Verify account exists
Account existingAccount = accountService.getByAccountId(account.getAccountId());
if(existingAccount == null){
errors.add("Account not found");
}
//Check login exists in entity
if((account.getUsername() == null) || (account.getUsername().isEmpty())){
errors.add("Invalid Login ID");
}
else{
//Check login doesn't exist in db, other than the object itself
Account existingAccountByLogin = accountService.getByUsername(account.getUsername());
if((existingAccountByLogin != null) && (!existingAccountByLogin.getAccountId().equals(account.getAccountId()))){
errors.add("Login ID already exists");
}
}
//Check email exists in entity
if((account.getEmail() == null) || (account.getEmail().isEmpty())){
errors.add("Invalid email");
}
else{
//Check email doesn't exist in db, other than the object itself
Account existingAccountByEmail = accountService.getByEmail(account.getEmail());
if((existingAccountByEmail != null) && (!existingAccountByEmail.getAccountId().equals(account.getAccountId()))){
errors.add("Account with email already exists");
}
}
return errors;
}
}

View File

@@ -0,0 +1,75 @@
package com.mattrixwv.raidbuilder.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mattrixwv.raidbuilder.exception.MissingAuthorizationException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestControllerAdvice
public class ExceptionController{
private static final ObjectMapper mapper = new ObjectMapper();
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ObjectNode genericExceptionHandler(Exception error){
log.error(error.getMessage(), error);
ObjectNode returnJson = mapper.createObjectNode();
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add(error.getMessage());
returnJson.set("errors", errorsNode);
returnJson.put("status", "error");
return returnJson;
}
@ExceptionHandler(AuthorizationDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ObjectNode authorizationExceptionHandler(AuthorizationDeniedException error){
log.info(error.getMessage());
ObjectNode returnJson = mapper.createObjectNode();
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add(error.getMessage());
returnJson.set("errors", errorsNode);
returnJson.put("status", "error");
return returnJson;
}
@ExceptionHandler(MissingAuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ObjectNode missingAuthorizationExceptionHandler(MissingAuthorizationException error){
log.info(error.getMessage());
ObjectNode returnJson = mapper.createObjectNode();
ArrayNode errorsNode = mapper.createArrayNode();
errorsNode.add("Server is misconfigured");
returnJson.set("errors", errorsNode);
returnJson.put("status", "error");
return returnJson;
}
}

View File

@@ -0,0 +1,56 @@
package com.mattrixwv.raidbuilder.entity;
import java.time.ZonedDateTime;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "account", schema = "raid_builder")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public class Account{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "account_id")
private UUID accountId;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "login_date")
private ZonedDateTime loginDate;
@Column(name = "email")
private String email;
@Column(name = "force_reset")
private boolean forceReset;
@Column(name = "refresh_token")
private UUID refreshToken;
@Column(name = "refresh_token_expiration")
private ZonedDateTime refreshTokenExpiration;
@Enumerated(EnumType.STRING)
@Column(name = "account_status")
private AccountStatus accountStatus;
@JsonIgnore
public String getPassword(){ return password; }
@JsonProperty
public void setPassword(String password){ this.password = password; }
}

View File

@@ -0,0 +1,44 @@
package com.mattrixwv.raidbuilder.entity;
import java.util.UUID;
import org.springframework.security.core.GrantedAuthority;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "account_permission", schema = "raid_builder")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public class AccountPermission implements GrantedAuthority{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "account_permission_id")
private UUID accountPermissionId;
@Column(name = "account_id")
private UUID accountId;
@Enumerated(EnumType.STRING)
@Column(name = "account_permission_type")
private AccountPermissionType accountPermissionType;
@Override
public String getAuthority(){
return accountPermissionType.name();
}
}

View File

@@ -0,0 +1,47 @@
package com.mattrixwv.raidbuilder.entity;
import java.util.UUID;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.TutorialStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "account_tutorial_status", schema = "raid_builder")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public class AccountTutorialStatus{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "account_tutorial_status_id")
private UUID accountTutorialStatusId;
@Column(name = "account_id")
private UUID accountId;
@Enumerated(EnumType.STRING)
@Column(name = "games_tutorial_status")
private TutorialStatus gamesTutorialStatus;
@Enumerated(EnumType.STRING)
@Column(name = "game_tutorial_status")
private TutorialStatus gameTutorialStatus;
@Enumerated(EnumType.STRING)
@Column(name = "raid_groups_tutorial_status")
private TutorialStatus raidGroupsTutorialStatus;
@Enumerated(EnumType.STRING)
@Column(name = "raid_group_tutorial_status")
private TutorialStatus raidGroupTutorialStatus;
@Enumerated(EnumType.STRING)
@Column(name = "instance_tutorial_status")
private TutorialStatus instanceTutorialStatus;
}

View File

@@ -0,0 +1,36 @@
package com.mattrixwv.raidbuilder.entity;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import lombok.Data;
@Data
public abstract class AuditableEntity{
@JsonIgnore
@LastModifiedBy
@Column(name = "modified_by")
protected UUID modifiedBy;
@JsonIgnore
@LastModifiedDate
@Column(name = "modified_date")
protected ZonedDateTime modifiedDate;
@JsonIgnore
@CreatedBy
@Column(name = "created_by", updatable = false)
protected UUID createdBy;
@JsonIgnore
@CreatedDate
@Column(name = "created_date", updatable = false)
protected ZonedDateTime createdDate;
}

View File

@@ -0,0 +1,56 @@
package com.mattrixwv.raidbuilder.entity;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.mattrixwv.raidbuilder.service.AccountService;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuditableEntityListener{
private final AccountService accountService;
@PrePersist
public void prePersist(AuditableEntity entity){
entity.setCreatedBy(getCurrentUserId());
entity.setCreatedDate(ZonedDateTime.now());
}
@PreUpdate
public void preUpdate(AuditableEntity entity){
entity.setModifiedBy(getCurrentUserId());
entity.setModifiedDate(ZonedDateTime.now());
}
private UUID getCurrentUserId(){
log.debug("Getting current auditor");
UUID returnUUID;
try{
UserDetails userDetails = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
returnUUID = accountService.getByUsername(userDetails.getUsername()).getAccountId();
}
catch(Exception e){
returnUUID = UUID.fromString("382b1ed8-7d5a-4683-a25d-1f462e9cd921");
log.debug("No user logged in: {}", returnUUID);
}
return returnUUID;
}
}

View File

@@ -0,0 +1,20 @@
package com.mattrixwv.raidbuilder.exception;
public class MissingAuthorizationException extends RuntimeException{
public MissingAuthorizationException(){
super();
}
public MissingAuthorizationException(String message){
super(message);
}
public MissingAuthorizationException(Throwable cause){
super(cause);
}
public MissingAuthorizationException(String message, Throwable cause){
super(message, cause);
}
}

View File

@@ -0,0 +1,5 @@
package com.mattrixwv.raidbuilder.repository.account;
public interface AccountCustomRepository{
}

View File

@@ -0,0 +1,15 @@
package com.mattrixwv.raidbuilder.repository.account;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import com.mattrixwv.raidbuilder.entity.Account;
public interface AccountRepository extends AccountCustomRepository, JpaRepository<Account, UUID>{
public Account findByUsername(String username);
public Account findByRefreshToken(UUID refreshToken);
public Account findByEmail(String email);
}

View File

@@ -0,0 +1,9 @@
package com.mattrixwv.raidbuilder.repository.account;
import org.springframework.stereotype.Repository;
@Repository
public class AccountRepositoryImpl implements AccountCustomRepository{
}

View File

@@ -0,0 +1,5 @@
package com.mattrixwv.raidbuilder.repository.account_permission;
public interface AccountPermissionCustomRepository{
}

View File

@@ -0,0 +1,14 @@
package com.mattrixwv.raidbuilder.repository.account_permission;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import com.mattrixwv.raidbuilder.entity.AccountPermission;
public interface AccountPermissionRepository extends AccountPermissionCustomRepository, JpaRepository<AccountPermission, UUID>{
public List<AccountPermission> findAllByAccountId(UUID accountId);
}

View File

@@ -0,0 +1,9 @@
package com.mattrixwv.raidbuilder.repository.account_permission;
import org.springframework.stereotype.Repository;
@Repository
public class AccountPermissionRepositoryImpl implements AccountPermissionCustomRepository{
}

View File

@@ -0,0 +1,5 @@
package com.mattrixwv.raidbuilder.repository.account_tutorial_status;
public interface AccountTutorialStatusCustomRepository{
}

View File

@@ -0,0 +1,12 @@
package com.mattrixwv.raidbuilder.repository.account_tutorial_status;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import com.mattrixwv.raidbuilder.entity.AccountTutorialStatus;
public interface AccountTutorialStatusRepository extends AccountTutorialStatusCustomRepository, JpaRepository<AccountTutorialStatus, UUID>{
}

View File

@@ -0,0 +1,9 @@
package com.mattrixwv.raidbuilder.repository.account_tutorial_status;
import org.springframework.stereotype.Repository;
@Repository
public class AccountTutorialStatusRepositoryImpl implements AccountTutorialStatusCustomRepository{
}

View File

@@ -0,0 +1,31 @@
package com.mattrixwv.raidbuilder.service;
import java.util.List;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mattrixwv.raidbuilder.entity.AccountPermission;
import com.mattrixwv.raidbuilder.repository.account_permission.AccountPermissionRepository;
import lombok.RequiredArgsConstructor;
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class AccountPermissionService{
private final AccountPermissionRepository accountPermissionRepository;
public AccountPermission createAccountPermission(AccountPermission accountPermission){
return accountPermissionRepository.save(accountPermission);
}
public List<AccountPermission> getByAccountId(UUID accountId){
return accountPermissionRepository.findAllByAccountId(accountId);
}
}

View File

@@ -0,0 +1,142 @@
package com.mattrixwv.raidbuilder.service;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mattrixwv.raidbuilder.entity.Account;
import com.mattrixwv.raidbuilder.entity.AccountPermission;
import com.mattrixwv.raidbuilder.entity.AccountTutorialStatus;
import com.mattrixwv.raidbuilder.repository.account.AccountRepository;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountStatus;
import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.TutorialStatus;
import com.mattrixwv.raidbuilder.util.UserPrincipal;
import lombok.RequiredArgsConstructor;
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class AccountService implements UserDetailsService{
private final PasswordEncoder passwordEncoder;
private final AccountRepository accountRepository;
private final AccountTutorialStatusService accountTutorialStatusService;
//Related services
private final AccountPermissionService accountPermissionService;
//Fields
@Value("${jwt.refreshTokenDuration}")
private Duration refreshTokenDuration;
//Write
public Account createAccount(Account account){
//Set default values
account.setAccountStatus(AccountStatus.UNCONFIRMED);
account.setPassword(passwordEncoder.encode(account.getPassword()));
account.setRefreshToken(UUID.randomUUID());
account.setRefreshTokenExpiration(ZonedDateTime.now().plus(refreshTokenDuration));
//Save account
account = accountRepository.save(account);
//Return the new account
return account;
}
public Account confirmAccount(Account account){
//Setup the confirmed values
account.setRefreshToken(null);
account.setRefreshTokenExpiration(null);
account.setAccountStatus(AccountStatus.ACTIVE);
//Save the account
account = accountRepository.save(account);
//Give account default permissions
AccountPermission accountPermission = new AccountPermission();
accountPermission.setAccountId(account.getAccountId());
accountPermission.setAccountPermissionType(AccountPermissionType.USER);
accountPermission = accountPermissionService.createAccountPermission(accountPermission);
//Give account default tutorial actions
AccountTutorialStatus accountTutorialStatus = new AccountTutorialStatus();
accountTutorialStatus.setAccountId(account.getAccountId());
accountTutorialStatus.setGamesTutorialStatus(TutorialStatus.NOT_COMPLETED);
accountTutorialStatus.setGameTutorialStatus(TutorialStatus.NOT_COMPLETED);
accountTutorialStatus.setRaidGroupsTutorialStatus(TutorialStatus.NOT_COMPLETED);
accountTutorialStatus.setRaidGroupTutorialStatus(TutorialStatus.NOT_COMPLETED);
accountTutorialStatus.setInstanceTutorialStatus(TutorialStatus.NOT_COMPLETED);
accountTutorialStatus = accountTutorialStatusService.createAccountTutorialStatus(accountTutorialStatus);
//Return the account
return account;
}
public Account updateAccount(Account account){
return accountRepository.save(account);
}
public Account updatePassword(UUID accountId, String password){
Account account = accountRepository.findById(accountId).orElse(null);
if(account != null){
account.setPassword(passwordEncoder.encode(password));
account = accountRepository.save(account);
}
return account;
}
//Read
public Account getByAccountId(UUID accountId){
return accountRepository.findById(accountId).orElse(null);
}
public Account getByUsername(String username){
return accountRepository.findByUsername(username);
}
public Account getByEmail(String email){
return accountRepository.findByEmail(email);
}
public Account getByRefreshToken(UUID refreshToken){
return accountRepository.findByRefreshToken(refreshToken);
}
//! UserDetailsService
@Override
public UserDetails loadUserByUsername(String username){
Account account = accountRepository.findByUsername(username);
//If no account with that username exists, throw an exception
if(account == null){
throw new UsernameNotFoundException(username);
}
//Update the login timestamp and refresh token
account.setLoginDate(ZonedDateTime.now());
account.setRefreshToken(UUID.randomUUID());
account.setRefreshTokenExpiration(ZonedDateTime.now().plus(refreshTokenDuration));
account = accountRepository.save(account);
//Get the account permissions
List<AccountPermission> accountPermissions = accountPermissionService.getByAccountId(account.getAccountId());
return new UserPrincipal(account, accountPermissions);
}
}

View File

@@ -0,0 +1,23 @@
package com.mattrixwv.raidbuilder.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mattrixwv.raidbuilder.entity.AccountTutorialStatus;
import com.mattrixwv.raidbuilder.repository.account_tutorial_status.AccountTutorialStatusRepository;
import lombok.RequiredArgsConstructor;
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class AccountTutorialStatusService{
private final AccountTutorialStatusRepository accountTutorialStatusRepository;
public AccountTutorialStatus createAccountTutorialStatus(AccountTutorialStatus accountTutorialStatus){
return accountTutorialStatusRepository.save(accountTutorialStatus);
}
}

View File

@@ -0,0 +1,26 @@
package com.mattrixwv.raidbuilder.util;
import lombok.experimental.UtilityClass;
@UtilityClass
public class DatabaseTypeUtil{
public static enum AccountStatus {
ACTIVE,
LOCKED,
INACTIVE,
DELETED,
UNCONFIRMED
};
public static enum AccountPermissionType {
ADMIN,
USER
}
public static enum TutorialStatus {
COMPLETED,
NOT_COMPLETED
};
}

View File

@@ -0,0 +1,40 @@
package com.mattrixwv.raidbuilder.util;
import java.util.Collection;
import java.util.List;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.mattrixwv.raidbuilder.entity.Account;
import com.mattrixwv.raidbuilder.entity.AccountPermission;
public class UserPrincipal implements UserDetails{
private Account account;
private List<AccountPermission> accountPermissions;
public UserPrincipal(@NonNull Account account, @NonNull List<AccountPermission> accountPermissions){
this.account = account;
this.accountPermissions = accountPermissions;
}
@Override
public String getUsername(){
return account.getUsername();
}
@Override
public String getPassword(){
return account.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return accountPermissions;
}
}

View File

@@ -0,0 +1,43 @@
{
"groups": [
{
"name": "jwt",
"description": "Properties for JWT management"
}
],
"properties": [
{
"name": "allowed-origins",
"type": "java.lang.String",
"description": "Allowed CORS origins",
"defaultValue": "http://localhost:3000"
},
{
"name": "jwt.access-token-duration",
"type": "java.time.Duration",
"description": "The duration for which the access token is valid",
"defaultValue": "900s"
},
{
"name": "jwt.refresh-token-duration",
"type": "java.time.Duration",
"description": "The duration for which the refresh token is valid",
"defaultValue": "30d"
}
],
"hints": [
{
"name": "allowed-origins",
"values": [
{
"value": "http://localhost:3000",
"description": "Local development"
},
{
"value": "https://api.raidbuilder.mattrixwv.com",
"description": "Production"
}
]
}
]
}

View File

@@ -0,0 +1,21 @@
#Server options
server.error.include-stacktrace=always
server.port=8001
server.shutdown=graceful
spring.lifecycle.timeoutPerShutdownPhase=10s
#Database options
spring.jpa.openInView=false
spring.datasource.url=jdbc:postgresql://localhost:5432/db_name?stringtype=unspecified
spring.datasource.username=username
spring.datasource.password=password
#CORS
#allowedOrigins=http://localhost:3000
allowedOrigins=*
#JWT
rsa.privateKey=classpath:certs/private.pem
rsa.publicKey=classpath:certs/public.pem
jwt.accessTokenDuration=15m
jwt.refreshTokenDuration=30d

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTPsOQkPNJbPi4
B2dlDKunPOL1Ehf7DXLDSablGzsunJk366or5AI1QbiCGeKU/jhTjsbZxXfcBVgt
n4BWxnaY6Ypay344LI/YYuSRieENcvZZuz9+kFwACpbO8+NIW1pGiPczOpUzFcSn
4BD36RnukeFOyDCiDlK82PF37hU/v79DAdQ7UAwK7M5xuzUbdRGebI7wRr++qbz5
9vcqLRqPY8JkEn0dmQ/7kOwRvqFBoCCVoVHThKPNWKxfkjTGKhrUGvAeUwl0A8MP
KNWDnLQ2HRfAS5UYh7BZ0Pp3CJupU6j3EzP+LNiKV7tQhZcGMcLclxjLNU6vMkS8
aiQFW7LfAgMBAAECggEAXEqBFZGGgQ9XcMtnTFonSocK3yg7CueauqBchp8JkblA
JZLUA533qv3eFxUpDZAt2q+3x+ACmEFLf48+emr12KO72yQprnAlnlPCaaV0CjSu
VZC90lVOpIP71EnwhCXJQKTJX3vaQHnjs7Zso2sXdcgNSCalPMAGPNSJVqzRYspf
mJxJbDn1iaZVfvBTsfbJQ02TXNA47UkPrQD/pBDegT34e0niLFTD4KY02OHSFg80
uKY/pGvGpdGuJkrkdv6wL5q81zDNp7YtGxFbVdtEGY3vLJgWwobI9sspnJKJ00Fr
HwYWUXPZQSwsU+I7jYI3/e+MRozola+EQCNmNJhXyQKBgQDm8t6h3jqkUq96+wXz
NYdMKSfE4zOk3CnZoq+iLXltC8B7Sns+y0+kuyHwlBsRGu1fQJwt2io2Vbxr94Qd
3YypprHE2WYDUFHwrHejcon8TwuB0zSllZE/zI+tWgma+1/hpCn4N7J8P37dRHTY
FvFU6U6qED3lSARjJo/tTbud9wKBgQDqKMEAzLdUy5opliwwG4+WAgtg2oNdWxFL
SFv1ErpHrlOILaoPVKb3Mfv68Ku/rpe6lmaNqasVLx1c3roh2nJhk8++bY+Y+JZu
25rIGO1BpcXobYzIQ7bjTYuvkFLDBmSy29NUaVyRT8h8114FwxurRSCif0jMp66v
KEjxjhp4WQKBgQCtM6vf/YhBQHm2Y5gcxCJJ7fuTX0mV9D+2ppnNqQkNzOh4Dm3L
tDJwup9Di++YrncjHpOCl8FcqoP6/NAqjcM2YHulw90L0ysAsnevLvFpNebNYJZ1
MGyUSlfejE3z214XHUUUkMDdCcmdK//tJ5eqNKb4R+IDmDUiHwOF1uxEFQKBgQDk
yRbykhLaXehtk5XvFy6u0aaOZlINx+nY1YVLqZWqbdCd8IgFXJ+aTRM3dylIKu2C
2GqxJULMevFEiTXx178ESeij1eaE/vX0sMrFkV1XVAJPe6IfFdI+usitq+TBOqDv
BMux4RQZwotQNxldpemF6Q/e1WCq3XdXGpRSt5ZzWQKBgCkzJi9/Cs4qn00iyhFC
h3dXfq3Des/ZYxGBJpw2/SAOBIti+QKQS7/UlPAml789cEO+B9w+dDrIENf4VJF0
0Zar2hI85bEnpUy+XeRwSo9vhgaYIox4k7B/FET4plqM1A6TyTGRy987CW9ua7ny
rQ9EYSbPs55Z4Rcv4O+DxAAL
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTPsOQkPNJbPi4
B2dlDKunPOL1Ehf7DXLDSablGzsunJk366or5AI1QbiCGeKU/jhTjsbZxXfcBVgt
n4BWxnaY6Ypay344LI/YYuSRieENcvZZuz9+kFwACpbO8+NIW1pGiPczOpUzFcSn
4BD36RnukeFOyDCiDlK82PF37hU/v79DAdQ7UAwK7M5xuzUbdRGebI7wRr++qbz5
9vcqLRqPY8JkEn0dmQ/7kOwRvqFBoCCVoVHThKPNWKxfkjTGKhrUGvAeUwl0A8MP
KNWDnLQ2HRfAS5UYh7BZ0Pp3CJupU6j3EzP+LNiKV7tQhZcGMcLclxjLNU6vMkS8
aiQFW7LfAgMBAAECggEAXEqBFZGGgQ9XcMtnTFonSocK3yg7CueauqBchp8JkblA
JZLUA533qv3eFxUpDZAt2q+3x+ACmEFLf48+emr12KO72yQprnAlnlPCaaV0CjSu
VZC90lVOpIP71EnwhCXJQKTJX3vaQHnjs7Zso2sXdcgNSCalPMAGPNSJVqzRYspf
mJxJbDn1iaZVfvBTsfbJQ02TXNA47UkPrQD/pBDegT34e0niLFTD4KY02OHSFg80
uKY/pGvGpdGuJkrkdv6wL5q81zDNp7YtGxFbVdtEGY3vLJgWwobI9sspnJKJ00Fr
HwYWUXPZQSwsU+I7jYI3/e+MRozola+EQCNmNJhXyQKBgQDm8t6h3jqkUq96+wXz
NYdMKSfE4zOk3CnZoq+iLXltC8B7Sns+y0+kuyHwlBsRGu1fQJwt2io2Vbxr94Qd
3YypprHE2WYDUFHwrHejcon8TwuB0zSllZE/zI+tWgma+1/hpCn4N7J8P37dRHTY
FvFU6U6qED3lSARjJo/tTbud9wKBgQDqKMEAzLdUy5opliwwG4+WAgtg2oNdWxFL
SFv1ErpHrlOILaoPVKb3Mfv68Ku/rpe6lmaNqasVLx1c3roh2nJhk8++bY+Y+JZu
25rIGO1BpcXobYzIQ7bjTYuvkFLDBmSy29NUaVyRT8h8114FwxurRSCif0jMp66v
KEjxjhp4WQKBgQCtM6vf/YhBQHm2Y5gcxCJJ7fuTX0mV9D+2ppnNqQkNzOh4Dm3L
tDJwup9Di++YrncjHpOCl8FcqoP6/NAqjcM2YHulw90L0ysAsnevLvFpNebNYJZ1
MGyUSlfejE3z214XHUUUkMDdCcmdK//tJ5eqNKb4R+IDmDUiHwOF1uxEFQKBgQDk
yRbykhLaXehtk5XvFy6u0aaOZlINx+nY1YVLqZWqbdCd8IgFXJ+aTRM3dylIKu2C
2GqxJULMevFEiTXx178ESeij1eaE/vX0sMrFkV1XVAJPe6IfFdI+usitq+TBOqDv
BMux4RQZwotQNxldpemF6Q/e1WCq3XdXGpRSt5ZzWQKBgCkzJi9/Cs4qn00iyhFC
h3dXfq3Des/ZYxGBJpw2/SAOBIti+QKQS7/UlPAml789cEO+B9w+dDrIENf4VJF0
0Zar2hI85bEnpUy+XeRwSo9vhgaYIox4k7B/FET4plqM1A6TyTGRy987CW9ua7ny
rQ9EYSbPs55Z4Rcv4O+DxAAL
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0z7DkJDzSWz4uAdnZQyr
pzzi9RIX+w1yw0mm5Rs7LpyZN+uqK+QCNUG4ghnilP44U47G2cV33AVYLZ+AVsZ2
mOmKWst+OCyP2GLkkYnhDXL2Wbs/fpBcAAqWzvPjSFtaRoj3MzqVMxXEp+AQ9+kZ
7pHhTsgwog5SvNjxd+4VP7+/QwHUO1AMCuzOcbs1G3URnmyO8Ea/vqm8+fb3Ki0a
j2PCZBJ9HZkP+5DsEb6hQaAglaFR04SjzVisX5I0xioa1BrwHlMJdAPDDyjVg5y0
Nh0XwEuVGIewWdD6dwibqVOo9xMz/izYile7UIWXBjHC3JcYyzVOrzJEvGokBVuy
3wIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,8 @@
#Create RSA key pair
openssl genrsa -out keypair.pem 2048
#Extract public key
openssl rsa -in keypair.pem -pubout -out public.pem
#Create private key in PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--<Configuration strict="true" xmlns="http://logging.apache.org/log4j/2.0/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://logging.apache.org/log4j/2.0/config https://raw.githubusercontent.com/apache/logging-log4j2/master/log4j-core/src/main/resources/Log4j-config.xsd">-->
<Configuration>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout disableAnsi="false">
<Pattern>
%style{%d{MM-dd-yyyy HH:mm:ss.SSSZ}}{bright, black} %highlight{%5p} %style{function=}{bright, black}%style{%50.50replace{%c{0}.%M}{}{}}{blue, bright} %style{requestId=%36.36X{requestId}}{bright, black} %style{username=%X{username}}{bright, black} %highlight{---} %message%n
</Pattern>
</PatternLayout>
</Console>
<RollingRandomAccessFile name="file" fileName="cipherStreamAPI.log" filePattern="%d{MM-dd-yyyy}-cipherStreamAPI.log.gz" immediateFlush="true">
<JsonTemplateLayout eventTemplateUri="classpath:template.json"></JsonTemplateLayout>
<Policies>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
<Console name="consoleJSON" target="SYSTEM_OUT">
<JsonTemplateLayout eventTemplateUri="classpath:template.json"/>
</Console>
<Socket name="graylog" host="loggingpi.mattrixwv.com" port="1502">
<JsonTemplateLayout eventTemplateUri="classpath:template.json"/>
</Socket>
</Appenders>
<Loggers>
<Root level="info" includeLocation="true">
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,46 @@
{
"msg_timestamp": {
"$resolver": "timestamp",
"pattern": {
"format": "yyyy-MM-dd HH:mm:ss.SSSZ"
}
},
"level": {
"$resolver": "level",
"field": "name"
},
"requestId": {
"$resolver": "mdc",
"key": "requestId"
},
"logger": {
"$resolver": "logger",
"field": "name"
},
"mdc":{
"$resolver": "mdc",
"flatten": false,
"stringified": false
},
"message": {
"$resolver": "message",
"stringified": true
},
"exception": {
"exception_class": {
"$resolver": "exception",
"field": "className"
},
"exception_message": {
"$resolver": "exception",
"field": "message"
},
"stacktrace": {
"$resolver": "exception",
"field": "stackTrace",
"stackTrace": {
"stringified": true
}
}
}
}

17
version-rules.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" comparisonMethod="maven"
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0 https://www.mojohaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
<ignoreVersions>
<!-- Ignore Alpha's, Beta's, release candidates and milestones -->
<ignoreVersion type="regex">(?i).*Alpha(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*a(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*Beta(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*-B(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*RC(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*CR(?:-?\d+)?</ignoreVersion>
<ignoreVersion type="regex">(?i).*M(?:-?\d+)?</ignoreVersion>
</ignoreVersions>
<rules>
</rules>
</ruleset>