diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java b/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java index b79019b..f98c1b6 100644 --- a/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java +++ b/src/main/java/com/mattrixwv/raidbuilder/config/SecurityConfig.java @@ -43,7 +43,8 @@ public class SecurityConfig{ http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> { - auth.requestMatchers("/auth/refresh").permitAll() //Permit refresh tokens + auth.requestMatchers(HttpMethod.OPTIONS).permitAll() + .requestMatchers("/auth/refresh", "/auth/test").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(); diff --git a/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java b/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java index 7235e3c..72ec2c2 100644 --- a/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java +++ b/src/main/java/com/mattrixwv/raidbuilder/config/WebConfig.java @@ -30,7 +30,8 @@ public class WebConfig implements WebMvcConfigurer{ registry.addMapping("/**") .allowedOriginPatterns(allowedOrigins) - .allowedMethods("GET", "PUT", "DELETE", "OPTIONS", "PATCH", "POST"); + .allowedMethods("GET", "PUT", "DELETE", "OPTIONS", "POST") + .allowCredentials(true); } diff --git a/src/main/java/com/mattrixwv/raidbuilder/controller/AccountController.java b/src/main/java/com/mattrixwv/raidbuilder/controller/AccountController.java new file mode 100644 index 0000000..45648eb --- /dev/null +++ b/src/main/java/com/mattrixwv/raidbuilder/controller/AccountController.java @@ -0,0 +1,233 @@ +package com.mattrixwv.raidbuilder.controller; + + +import java.util.List; +import java.util.UUID; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.entity.Account; +import com.mattrixwv.raidbuilder.service.AccountService; +import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@RestController +@RequestMapping("/account") +@RequiredArgsConstructor +public class AccountController{ + private final ObjectMapper mapper; + private final AccountService accountService; + + + @GetMapping + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public List getAccounts(@RequestParam("page") int page, @RequestParam("pageSize") int pageSize, @RequestParam(value = "searchTerm", required = false) String searchTerm){ + log.info("Getting accounts page {} of size {} with search term {}", page, pageSize, searchTerm); + + + List accounts; + if((searchTerm == null) || (searchTerm.isEmpty())){ + accounts = accountService.getAccounts(page, pageSize); + } + else{ + accounts = accountService.getAccounts(page, pageSize, searchTerm); + } + + + return accounts; + } + + @PostMapping + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode createAccount(@RequestBody Account account){ + log.info("Creating account {}", account.getUsername()); + + + ObjectNode returnNode = mapper.createObjectNode(); + Account existingAccount = accountService.getByUsername(account.getUsername()); + if(existingAccount != null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Username already exists"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + account = accountService.createAccount(account); + returnNode.put("status", "success"); + returnNode.put("accountId", account.getAccountId().toString()); + } + + + return returnNode; + } + + @GetMapping("/count") + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode getAccountsCount(@RequestParam(value = "searchTerm", required = false) String searchTerm){ + log.info("Getting accounts count"); + + + Long accountsCount; + if((searchTerm == null) || (searchTerm.isBlank())){ + accountsCount = accountService.getAccountsCount(); + } + else{ + accountsCount = accountService.getAccountsCount(searchTerm); + } + + ObjectNode countNode = mapper.createObjectNode(); + countNode.put("count", accountsCount); + countNode.put("status", "success"); + + + return countNode; + } + + @PutMapping("/{accountId}/forcePasswordReset") + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode forcePasswordReset(@PathVariable("accountId") UUID accountId){ + log.info("Forcing password reset for account {}", accountId); + + + Account account = accountService.getByAccountId(accountId); + ObjectNode returnNode = mapper.createObjectNode(); + if(account == null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Account not found"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + account.setRefreshToken(null); + account.setRefreshTokenExpiration(null); + account.setForceReset(true); + accountService.updateAccount(account); + + returnNode.put("status", "success"); + } + + + return returnNode; + } + + @PutMapping("/{accountId}/resetPassword") + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode resetPassword(@PathVariable("accountId") UUID accountId, @RequestBody ObjectNode passwordNode){ + log.info("Resetting password for account {}", accountId); + + + Account account = accountService.getByAccountId(accountId); + ObjectNode returnNode = mapper.createObjectNode(); + if(account == null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Account not found"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + account.setRefreshToken(null); + account.setRefreshTokenExpiration(null); + account.setForceReset(false); + accountService.updateAccount(account); + accountService.updatePassword(accountId, passwordNode.get("password").asText()); + + 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); + ObjectNode returnNode = mapper.createObjectNode(); + if(account == null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Account not found"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + account.setRefreshToken(null); + account.setRefreshTokenExpiration(null); + accountService.updateAccount(account); + + returnNode.put("status", "success"); + } + + + return returnNode; + } + + @PutMapping("/{accountId}") + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode updateAccount(@PathVariable("accountId") UUID accountId, @RequestBody Account account){ + log.info("Updating account {}", accountId); + + + Account oldAccount = accountService.getByAccountId(accountId); + ObjectNode returnNode = mapper.createObjectNode(); + if(oldAccount == null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Account not found"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + oldAccount.setUsername(account.getUsername()); + oldAccount.setEmail(account.getEmail()); + oldAccount.setAccountStatus(account.getAccountStatus()); + accountService.updateAccount(oldAccount); + + returnNode.put("status", "success"); + } + + + return returnNode; + } + + @DeleteMapping("/{accountId}") + @AccountAuthorization(permissions = {AccountPermissionType.ADMIN}) + public ObjectNode deleteAccount(@PathVariable("accountId") UUID accountId){ + log.info("Deleting account {}", accountId); + + + Account account = accountService.getByAccountId(accountId); + ObjectNode returnNode = mapper.createObjectNode(); + if(account == null){ + ArrayNode errorsNode = mapper.createArrayNode(); + errorsNode.add("Account not found"); + returnNode.set("errors", errorsNode); + returnNode.put("status", "error"); + } + else{ + accountService.deleteAccount(accountId); + + returnNode.put("status", "success"); + } + + + return returnNode; + } +} diff --git a/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java b/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java index a8b5068..5d4f2ad 100644 --- a/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java +++ b/src/main/java/com/mattrixwv/raidbuilder/controller/AuthenticationController.java @@ -8,6 +8,7 @@ import java.util.UUID; import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -26,6 +27,9 @@ import com.mattrixwv.raidbuilder.service.AccountService; import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountPermissionType; import com.mattrixwv.raidbuilder.util.DatabaseTypeUtil.AccountStatus; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -39,9 +43,10 @@ public class AuthenticationController{ private final TokenService tokenService; private final AccountService accountService; - @PostMapping("/token") + + @GetMapping("/token") @AccountAuthorization(permissions = {}) - public ObjectNode token(Authentication authentication){ + public ObjectNode token(Authentication authentication, HttpServletResponse response){ log.info("Token requested for user {}", authentication.getName()); @@ -50,19 +55,35 @@ public class AuthenticationController{ log.debug("Token granted {}", token); ObjectNode tokenNode = mapper.valueToTree(account); tokenNode.put("token", token); + Cookie cookie = new Cookie("refreshToken", account.getRefreshToken().toString()); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); return tokenNode; } - @PostMapping("/refresh") + @GetMapping("/refresh") @AccountAuthorization(permissions = {}) - public ObjectNode refresh(@RequestBody ObjectNode refreshTokenNode){ + public ObjectNode refresh(HttpServletRequest request) throws InterruptedException{ log.info("Refreshing token"); - UUID refreshToken = UUID.fromString(refreshTokenNode.get("refreshToken").asText()); - log.debug("refreshToken: {}", refreshToken); + Thread.sleep(2000); + UUID refreshToken = null; + if(request.getCookies() != null){ + for(Cookie cookie : request.getCookies()){ + if(cookie.getName().equals("refreshToken")){ + log.debug("refreshToken = {}", refreshToken); + refreshToken = UUID.fromString(cookie.getValue()); + } + } + } + + if(refreshToken == null){ + throw new AuthorizationDeniedException("Refresh token is missing", () -> false); + } Account account = accountService.getByRefreshToken(refreshToken); @@ -226,9 +247,9 @@ public class AuthenticationController{ return returnNode; } - @PostMapping("/logout") + @GetMapping("/logout") @AccountAuthorization(permissions = {}) - public ObjectNode logout(Authentication authentication){ + public ObjectNode logout(Authentication authentication, HttpServletResponse response){ log.info("Logging out account {}", authentication.getName()); @@ -238,6 +259,10 @@ public class AuthenticationController{ account.setRefreshTokenExpiration(null); account = accountService.updateAccount(account); } + Cookie cookie = new Cookie("refreshToken", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); ObjectNode returnNode = mapper.createObjectNode(); returnNode.put("status", "success"); diff --git a/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java index 1f070d4..9d0179e 100644 --- a/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java +++ b/src/main/java/com/mattrixwv/raidbuilder/repository/account/AccountRepository.java @@ -1,8 +1,10 @@ package com.mattrixwv.raidbuilder.repository.account; +import java.util.List; import java.util.UUID; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import com.mattrixwv.raidbuilder.entity.Account; @@ -12,4 +14,7 @@ public interface AccountRepository extends AccountCustomRepository, JpaRepositor public Account findByUsername(String username); public Account findByRefreshToken(UUID refreshToken); public Account findByEmail(String email); + + public List findAllByUsernameContainingIgnoreCase(String searchTerm, PageRequest pageRequest); + public long countByUsernameContainingIgnoreCase(String searchTerm); } diff --git a/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java b/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java index bd655ba..c263336 100644 --- a/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java +++ b/src/main/java/com/mattrixwv/raidbuilder/service/AccountService.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -99,6 +101,9 @@ public class AccountService implements UserDetailsService{ return account; } + public void deleteAccount(UUID accountId){ + accountRepository.deleteById(accountId); + } //Read public Account getByAccountId(UUID accountId){ @@ -117,6 +122,22 @@ public class AccountService implements UserDetailsService{ return accountRepository.findByRefreshToken(refreshToken); } + public List getAccounts(int page, int pageSize){ + return accountRepository.findAll(PageRequest.of(page, pageSize, Sort.by("username").ascending())).getContent(); + } + + public List getAccounts(int page, int pageSize, String searchTerm){ + return accountRepository.findAllByUsernameContainingIgnoreCase(searchTerm, PageRequest.of(page, pageSize, Sort.by("username").ascending())); + } + + public long getAccountsCount(){ + return accountRepository.count(); + } + + public long getAccountsCount(String searchTerm){ + return accountRepository.countByUsernameContainingIgnoreCase(searchTerm); + } + //! UserDetailsService @Override diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7e29d02..b3962c6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,7 +12,7 @@ spring.datasource.password=password #CORS #allowedOrigins=http://localhost:3000 -allowedOrigins=* +#allowedOrigins=* #JWT rsa.privateKey=classpath:certs/private.pem