diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0112546 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "configurations": [ + { + "type": "java", + "name": "Spring Boot-RaidBuilderAPI", + "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" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a3cc8af --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "raidbuilder", + "springframework" + ] +} \ No newline at end of file diff --git a/SpringStuffToIgnoreInExceptions.txt b/SpringStuffToIgnoreInExceptions.txt new file mode 100644 index 0000000..65a49ed --- /dev/null +++ b/SpringStuffToIgnoreInExceptions.txt @@ -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 diff --git a/db/1.0.0/1. schema.sql b/db/1.0.0/1. schema.sql new file mode 100644 index 0000000..38c5dbf --- /dev/null +++ b/db/1.0.0/1. schema.sql @@ -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; diff --git a/db/1.0.0/2. createAccount.sql b/db/1.0.0/2. createAccount.sql new file mode 100644 index 0000000..2ca3e93 --- /dev/null +++ b/db/1.0.0/2. createAccount.sql @@ -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; diff --git a/db/1.0.0/3. createAccountPermission.sql b/db/1.0.0/3. createAccountPermission.sql new file mode 100644 index 0000000..ef6860a --- /dev/null +++ b/db/1.0.0/3. createAccountPermission.sql @@ -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; diff --git a/db/1.0.0/4. createAccountTutorialStatus.sql b/db/1.0.0/4. createAccountTutorialStatus.sql new file mode 100644 index 0000000..86cc0a4 --- /dev/null +++ b/db/1.0.0/4. createAccountTutorialStatus.sql @@ -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; diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..624a1a2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + com.mattrixwv.raidbuilder + raid-builder-api + jar + 1.0.0-SNAPSHOT + Raid Builder API + https://api.raidbuilder.mattrixwv.com + + + + UTF-8 + 21 + 21 + 21 + + + 21 + target/dependency-check-report.json + target/dependency-check-report.html + + + + org.springframework.boot + spring-boot-starter-parent + 3.4.3 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-config + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework + spring-aspects + + + org.springframework.boot + spring-boot-devtools + true + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + + + + org.postgresql + postgresql + 42.7.5 + + + io.hypersistence + hypersistence-utils-hibernate-63 + 3.9.2 + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.18.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate6 + 2.18.2 + + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + org.slf4j + slf4j-api + 2.0.16 + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.apache.logging.log4j + log4j-layout-template-json + 2.24.3 + + + com.lmax + disruptor + 4.0.0 + + + diff --git a/src/main/java/com/mattrixwv/raidbuilder/RaidBuilderAPI.java b/src/main/java/com/mattrixwv/raidbuilder/RaidBuilderAPI.java new file mode 100644 index 0000000..ca00652 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/RaidBuilderAPI.java @@ -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); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/annotation/AccountAuthorization.java b/src/main/java/com/mattrixwv/raidbuilder/annotation/AccountAuthorization.java new file mode 100644 index 0000000..fc64c05 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/annotation/AccountAuthorization.java @@ -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(); +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/aspect/AccountAuthorizationAspect.java b/src/main/java/com/mattrixwv/raidbuilder/aspect/AccountAuthorizationAspect.java new file mode 100644 index 0000000..a654e99 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/aspect/AccountAuthorizationAspect.java @@ -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 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(); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/RsaKeyProperties.java b/src/main/java/com/mattrixwv/raidbuilder/config/RsaKeyProperties.java new file mode 100644 index 0000000..52a4d72 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/config/RsaKeyProperties.java @@ -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){ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java b/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java new file mode 100644 index 0000000..b79019b --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java @@ -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 jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); + + return new NimbusJwtEncoder(jwks); + } + + @Bean + public JwtDecoder jwtDecoder(){ + return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build(); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/TokenService.java b/src/main/java/com/mattrixwv/raidbuilder/config/TokenService.java new file mode 100644 index 0000000..2bd4b23 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/config/TokenService.java @@ -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(); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java b/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java new file mode 100644 index 0000000..7235e3c --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java @@ -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; + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/WebFilter.java b/src/main/java/com/mattrixwv/raidbuilder/config/WebFilter.java new file mode 100644 index 0000000..fbc102c --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/config/WebFilter.java @@ -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()); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java b/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java new file mode 100644 index 0000000..a8b5068 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java @@ -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 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 verifyNewAccount(Account account){ + ArrayList 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 verifyUpdatedAccount(Account account){ + ArrayList 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; + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/controller/ExceptionController.java b/src/main/java/com/mattrixwv/raidbuilder/controller/ExceptionController.java new file mode 100644 index 0000000..b109321 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/controller/ExceptionController.java @@ -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; + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/entity/Account.java b/src/main/java/com/mattrixwv/raidbuilder/entity/Account.java new file mode 100644 index 0000000..3fe6994 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/entity/Account.java @@ -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; } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/entity/AccountPermission.java b/src/main/java/com/mattrixwv/raidbuilder/entity/AccountPermission.java new file mode 100644 index 0000000..1e6450b --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/entity/AccountPermission.java @@ -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(); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/entity/AccountTutorialStatus.java b/src/main/java/com/mattrixwv/raidbuilder/entity/AccountTutorialStatus.java new file mode 100644 index 0000000..fa1400f --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/entity/AccountTutorialStatus.java @@ -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; +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntity.java b/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntity.java new file mode 100644 index 0000000..2774c46 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntity.java @@ -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; +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntityListener.java b/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntityListener.java new file mode 100644 index 0000000..dc78207 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/entity/AuditableEntityListener.java @@ -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; + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/exception/MissingAuthorizationException.java b/src/main/java/com/mattrixwv/raidbuilder/exception/MissingAuthorizationException.java new file mode 100644 index 0000000..7d8148c --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/exception/MissingAuthorizationException.java @@ -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); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountCustomRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountCustomRepository.java new file mode 100644 index 0000000..0e26559 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountCustomRepository.java @@ -0,0 +1,5 @@ +package com.mattrixwv.raidbuilder.repository.account; + + +public interface AccountCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java new file mode 100644 index 0000000..1f070d4 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java @@ -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{ + public Account findByUsername(String username); + public Account findByRefreshToken(UUID refreshToken); + public Account findByEmail(String email); +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepositoryImpl.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepositoryImpl.java new file mode 100644 index 0000000..c58ee2b --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepositoryImpl.java @@ -0,0 +1,9 @@ +package com.mattrixwv.raidbuilder.repository.account; + + +import org.springframework.stereotype.Repository; + + +@Repository +public class AccountRepositoryImpl implements AccountCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionCustomRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionCustomRepository.java new file mode 100644 index 0000000..81578a7 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionCustomRepository.java @@ -0,0 +1,5 @@ +package com.mattrixwv.raidbuilder.repository.account_permission; + + +public interface AccountPermissionCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepository.java new file mode 100644 index 0000000..b38380a --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepository.java @@ -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{ + public List findAllByAccountId(UUID accountId); +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepositoryImpl.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepositoryImpl.java new file mode 100644 index 0000000..feac866 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_permission/AccountPermissionRepositoryImpl.java @@ -0,0 +1,9 @@ +package com.mattrixwv.raidbuilder.repository.account_permission; + + +import org.springframework.stereotype.Repository; + + +@Repository +public class AccountPermissionRepositoryImpl implements AccountPermissionCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusCustomRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusCustomRepository.java new file mode 100644 index 0000000..9a6e883 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusCustomRepository.java @@ -0,0 +1,5 @@ +package com.mattrixwv.raidbuilder.repository.account_tutorial_status; + + +public interface AccountTutorialStatusCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepository.java new file mode 100644 index 0000000..fbdb2a1 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepository.java @@ -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{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepositoryImpl.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepositoryImpl.java new file mode 100644 index 0000000..d98dc09 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account_tutorial_status/AccountTutorialStatusRepositoryImpl.java @@ -0,0 +1,9 @@ +package com.mattrixwv.raidbuilder.repository.account_tutorial_status; + + +import org.springframework.stereotype.Repository; + + +@Repository +public class AccountTutorialStatusRepositoryImpl implements AccountTutorialStatusCustomRepository{ +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/service/AccountPermissionService.java b/src/main/java/com/mattrixwv/raidbuilder/service/AccountPermissionService.java new file mode 100644 index 0000000..35e6fb0 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/service/AccountPermissionService.java @@ -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 getByAccountId(UUID accountId){ + return accountPermissionRepository.findAllByAccountId(accountId); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java b/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java new file mode 100644 index 0000000..bd655ba --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java @@ -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 accountPermissions = accountPermissionService.getByAccountId(account.getAccountId()); + + return new UserPrincipal(account, accountPermissions); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/service/AccountTutorialStatusService.java b/src/main/java/com/mattrixwv/raidbuilder/service/AccountTutorialStatusService.java new file mode 100644 index 0000000..8a8d27e --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/service/AccountTutorialStatusService.java @@ -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); + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/util/DatabaseTypeUtil.java b/src/main/java/com/mattrixwv/raidbuilder/util/DatabaseTypeUtil.java new file mode 100644 index 0000000..5ab6956 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/util/DatabaseTypeUtil.java @@ -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 + }; +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/util/UserPrincipal.java b/src/main/java/com/mattrixwv/raidbuilder/util/UserPrincipal.java new file mode 100644 index 0000000..a9ad5f7 --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/util/UserPrincipal.java @@ -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 accountPermissions; + + + public UserPrincipal(@NonNull Account account, @NonNull List accountPermissions){ + this.account = account; + this.accountPermissions = accountPermissions; + } + + + @Override + public String getUsername(){ + return account.getUsername(); + } + + @Override + public String getPassword(){ + return account.getPassword(); + } + + @Override + public Collection getAuthorities(){ + return accountPermissions; + } +} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..42711b2 --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..7e29d02 --- /dev/null +++ b/src/main/resources/application.properties @@ -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 diff --git a/src/main/resources/certs/keypair.pem b/src/main/resources/certs/keypair.pem new file mode 100644 index 0000000..6a0f667 --- /dev/null +++ b/src/main/resources/certs/keypair.pem @@ -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----- diff --git a/src/main/resources/certs/private.pem b/src/main/resources/certs/private.pem new file mode 100644 index 0000000..6a0f667 --- /dev/null +++ b/src/main/resources/certs/private.pem @@ -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----- diff --git a/src/main/resources/certs/public.pem b/src/main/resources/certs/public.pem new file mode 100644 index 0000000..bdb8a8e --- /dev/null +++ b/src/main/resources/certs/public.pem @@ -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----- diff --git a/src/main/resources/generateCerts.sh b/src/main/resources/generateCerts.sh new file mode 100644 index 0000000..aec4cfc --- /dev/null +++ b/src/main/resources/generateCerts.sh @@ -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 diff --git a/src/main/resources/log4j2-spring.xml b/src/main/resources/log4j2-spring.xml new file mode 100644 index 0000000..fc03b92 --- /dev/null +++ b/src/main/resources/log4j2-spring.xml @@ -0,0 +1,39 @@ + + + + + + + + + %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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/template.json b/src/main/resources/template.json new file mode 100644 index 0000000..cf76113 --- /dev/null +++ b/src/main/resources/template.json @@ -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 + } + } + } +} diff --git a/version-rules.xml b/version-rules.xml new file mode 100644 index 0000000..7ab49c9 --- /dev/null +++ b/version-rules.xml @@ -0,0 +1,17 @@ + + + + + (?i).*Alpha(?:-?\d+)? + (?i).*a(?:-?\d+)? + (?i).*Beta(?:-?\d+)? + (?i).*-B(?:-?\d+)? + (?i).*RC(?:-?\d+)? + (?i).*CR(?:-?\d+)? + (?i).*M(?:-?\d+)? + + + +