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

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;
}
}