diff --git a/pom.xml b/pom.xml index 9c14b26..a90be1a 100644 --- a/pom.xml +++ b/pom.xml @@ -31,14 +31,14 @@ - org.postgresql - postgresql - runtime + org.jetbrains + annotations + 24.0.1 - org.projectlombok - lombok - true + com.mysql + mysql-connector-j + runtime io.jsonwebtoken @@ -65,16 +65,6 @@ spring-boot-starter-validation - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - diff --git a/src/main/java/com/alibou/security/Application.java b/src/main/java/com/alibou/security/Application.java new file mode 100644 index 0000000..b2c93d9 --- /dev/null +++ b/src/main/java/com/alibou/security/Application.java @@ -0,0 +1,13 @@ +package com.alibou.security; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@SpringBootApplication +@EnableJpaAuditing(auditorAwareRef = "auditorAware") +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/alibou/security/SecurityApplication.java b/src/main/java/com/alibou/security/SecurityApplication.java deleted file mode 100644 index 1448cff..0000000 --- a/src/main/java/com/alibou/security/SecurityApplication.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.alibou.security; - -import com.alibou.security.auth.AuthenticationService; -import com.alibou.security.auth.RegisterRequest; -import com.alibou.security.user.Role; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -import static com.alibou.security.user.Role.ADMIN; -import static com.alibou.security.user.Role.MANAGER; - -@SpringBootApplication -@EnableJpaAuditing(auditorAwareRef = "auditorAware") -public class SecurityApplication { - - public static void main(String[] args) { - SpringApplication.run(SecurityApplication.class, args); - } - - @Bean - public CommandLineRunner commandLineRunner( - AuthenticationService service - ) { - return args -> { - var admin = RegisterRequest.builder() - .firstname("Admin") - .lastname("Admin") - .email("admin@mail.com") - .password("password") - .role(ADMIN) - .build(); - System.out.println("Admin token: " + service.register(admin).getAccessToken()); - - var manager = RegisterRequest.builder() - .firstname("Admin") - .lastname("Admin") - .email("manager@mail.com") - .password("password") - .role(MANAGER) - .build(); - System.out.println("Manager token: " + service.register(manager).getAccessToken()); - - }; - } -} diff --git a/src/main/java/com/alibou/security/auth/AuthenticationController.java b/src/main/java/com/alibou/security/auth/AuthenticationController.java deleted file mode 100644 index e1d5107..0000000 --- a/src/main/java/com/alibou/security/auth/AuthenticationController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.alibou.security.auth; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; - -@RestController -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class AuthenticationController { - - private final AuthenticationService service; - - @PostMapping("/register") - public ResponseEntity register( - @RequestBody RegisterRequest request - ) { - return ResponseEntity.ok(service.register(request)); - } - @PostMapping("/authenticate") - public ResponseEntity authenticate( - @RequestBody AuthenticationRequest request - ) { - return ResponseEntity.ok(service.authenticate(request)); - } - - @PostMapping("/refresh-token") - public void refreshToken( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - service.refreshToken(request, response); - } - - -} diff --git a/src/main/java/com/alibou/security/auth/AuthenticationRequest.java b/src/main/java/com/alibou/security/auth/AuthenticationRequest.java deleted file mode 100644 index 6d72722..0000000 --- a/src/main/java/com/alibou/security/auth/AuthenticationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.alibou.security.auth; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class AuthenticationRequest { - - private String email; - String password; -} diff --git a/src/main/java/com/alibou/security/auth/AuthenticationResponse.java b/src/main/java/com/alibou/security/auth/AuthenticationResponse.java deleted file mode 100644 index c10bbb6..0000000 --- a/src/main/java/com/alibou/security/auth/AuthenticationResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.alibou.security.auth; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class AuthenticationResponse { - - @JsonProperty("access_token") - private String accessToken; - @JsonProperty("refresh_token") - private String refreshToken; -} diff --git a/src/main/java/com/alibou/security/auth/AuthenticationService.java b/src/main/java/com/alibou/security/auth/AuthenticationService.java deleted file mode 100644 index 53193a7..0000000 --- a/src/main/java/com/alibou/security/auth/AuthenticationService.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.alibou.security.auth; - -import com.alibou.security.config.JwtService; -import com.alibou.security.token.Token; -import com.alibou.security.token.TokenRepository; -import com.alibou.security.token.TokenType; -import com.alibou.security.user.Role; -import com.alibou.security.user.User; -import com.alibou.security.user.UserRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Service; - -import java.io.IOException; - -@Service -@RequiredArgsConstructor -public class AuthenticationService { - private final UserRepository repository; - private final TokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - private final JwtService jwtService; - private final AuthenticationManager authenticationManager; - - public AuthenticationResponse register(RegisterRequest request) { - var user = User.builder() - .firstname(request.getFirstname()) - .lastname(request.getLastname()) - .email(request.getEmail()) - .password(passwordEncoder.encode(request.getPassword())) - .role(request.getRole()) - .build(); - var savedUser = repository.save(user); - var jwtToken = jwtService.generateToken(user); - var refreshToken = jwtService.generateRefreshToken(user); - saveUserToken(savedUser, jwtToken); - return AuthenticationResponse.builder() - .accessToken(jwtToken) - .refreshToken(refreshToken) - .build(); - } - - public AuthenticationResponse authenticate(AuthenticationRequest request) { - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - request.getEmail(), - request.getPassword() - ) - ); - var user = repository.findByEmail(request.getEmail()) - .orElseThrow(); - var jwtToken = jwtService.generateToken(user); - var refreshToken = jwtService.generateRefreshToken(user); - revokeAllUserTokens(user); - saveUserToken(user, jwtToken); - return AuthenticationResponse.builder() - .accessToken(jwtToken) - .refreshToken(refreshToken) - .build(); - } - - private void saveUserToken(User user, String jwtToken) { - var token = Token.builder() - .user(user) - .token(jwtToken) - .tokenType(TokenType.BEARER) - .expired(false) - .revoked(false) - .build(); - tokenRepository.save(token); - } - - private void revokeAllUserTokens(User user) { - var validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId()); - if (validUserTokens.isEmpty()) - return; - validUserTokens.forEach(token -> { - token.setExpired(true); - token.setRevoked(true); - }); - tokenRepository.saveAll(validUserTokens); - } - - public void refreshToken( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - final String refreshToken; - final String userEmail; - if (authHeader == null ||!authHeader.startsWith("Bearer ")) { - return; - } - refreshToken = authHeader.substring(7); - userEmail = jwtService.extractUsername(refreshToken); - if (userEmail != null) { - var user = this.repository.findByEmail(userEmail) - .orElseThrow(); - if (jwtService.isTokenValid(refreshToken, user)) { - var accessToken = jwtService.generateToken(user); - revokeAllUserTokens(user); - saveUserToken(user, accessToken); - var authResponse = AuthenticationResponse.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); - new ObjectMapper().writeValue(response.getOutputStream(), authResponse); - } - } - } -} diff --git a/src/main/java/com/alibou/security/auth/RegisterRequest.java b/src/main/java/com/alibou/security/auth/RegisterRequest.java deleted file mode 100644 index 4f51665..0000000 --- a/src/main/java/com/alibou/security/auth/RegisterRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.alibou.security.auth; - -import com.alibou.security.user.Role; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class RegisterRequest { - - private String firstname; - private String lastname; - private String email; - private String password; - private Role role; -} diff --git a/src/main/java/com/alibou/security/auth/controller/AuthenticationController.java b/src/main/java/com/alibou/security/auth/controller/AuthenticationController.java new file mode 100644 index 0000000..4c3a7fd --- /dev/null +++ b/src/main/java/com/alibou/security/auth/controller/AuthenticationController.java @@ -0,0 +1,47 @@ +package com.alibou.security.auth.controller; + +import com.alibou.security.auth.model.AuthenticationRequest; +import com.alibou.security.auth.model.AuthenticationResponse; +import com.alibou.security.auth.service.AuthenticationService; +import com.alibou.security.auth.model.RegisterRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +@RestController +@RequestMapping("/api/v1/auth") +public class AuthenticationController { + + private final AuthenticationService service; + + public AuthenticationController(AuthenticationService service) { + this.service = service; + } + + @PostMapping("/register") + public ResponseEntity register( + @RequestBody RegisterRequest request + ) { + return ResponseEntity.ok(service.register(request)); + } + + @PostMapping("/authenticate") + public ResponseEntity authenticate( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.authenticate(request)); + } + + @PostMapping("/refresh-token") + public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException { + service.refreshToken(request, response); + } + + +} diff --git a/src/main/java/com/alibou/security/auth/model/AuthenticationRequest.java b/src/main/java/com/alibou/security/auth/model/AuthenticationRequest.java new file mode 100644 index 0000000..cab7c01 --- /dev/null +++ b/src/main/java/com/alibou/security/auth/model/AuthenticationRequest.java @@ -0,0 +1,7 @@ +package com.alibou.security.auth.model; + + +import org.jetbrains.annotations.NotNull; + +public record AuthenticationRequest(@NotNull String email, @NotNull String password) { +} diff --git a/src/main/java/com/alibou/security/auth/model/AuthenticationResponse.java b/src/main/java/com/alibou/security/auth/model/AuthenticationResponse.java new file mode 100644 index 0000000..be9213e --- /dev/null +++ b/src/main/java/com/alibou/security/auth/model/AuthenticationResponse.java @@ -0,0 +1,8 @@ +package com.alibou.security.auth.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + + +public record AuthenticationResponse(@JsonProperty("access_token") String accessToken, + @JsonProperty("refresh_token") String refreshToken) { +} diff --git a/src/main/java/com/alibou/security/auth/model/RegisterRequest.java b/src/main/java/com/alibou/security/auth/model/RegisterRequest.java new file mode 100644 index 0000000..d95ccd6 --- /dev/null +++ b/src/main/java/com/alibou/security/auth/model/RegisterRequest.java @@ -0,0 +1,13 @@ +package com.alibou.security.auth.model; + +import com.alibou.security.user.model.Role; +import org.jetbrains.annotations.NotNull; + + +public record RegisterRequest(@NotNull String firstname, + @NotNull String lastname, + @NotNull String email, + @NotNull String password, + @NotNull Role role) { + +} diff --git a/src/main/java/com/alibou/security/auth/model/Token.java b/src/main/java/com/alibou/security/auth/model/Token.java new file mode 100644 index 0000000..062b748 --- /dev/null +++ b/src/main/java/com/alibou/security/auth/model/Token.java @@ -0,0 +1,92 @@ +package com.alibou.security.auth.model; + +import com.alibou.security.user.model.User; +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; + +import java.util.Objects; + +@Entity +@Table(name = "token") +public class Token { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native") + public Long id; + + @Column(unique = true) + public String token; + + @Enumerated(EnumType.STRING) + public TokenType tokenType = TokenType.BEARER; + + public boolean revoked; + + public boolean expired; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + public User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public TokenType getTokenType() { + return tokenType; + } + + public void setTokenType(TokenType tokenType) { + this.tokenType = tokenType; + } + + public boolean isRevoked() { + return revoked; + } + + public void setRevoked(boolean revoked) { + this.revoked = revoked; + } + + public boolean isExpired() { + return expired; + } + + public void setExpired(boolean expired) { + this.expired = expired; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Token token1 = (Token) o; + return revoked == token1.revoked && expired == token1.expired && Objects.equals(id, token1.id) && Objects.equals(token, token1.token) && tokenType == token1.tokenType && Objects.equals(user, token1.user); + } + + @Override + public int hashCode() { + return Objects.hash(id, token, tokenType, revoked, expired, user); + } +} diff --git a/src/main/java/com/alibou/security/auth/model/TokenType.java b/src/main/java/com/alibou/security/auth/model/TokenType.java new file mode 100644 index 0000000..b685f59 --- /dev/null +++ b/src/main/java/com/alibou/security/auth/model/TokenType.java @@ -0,0 +1,5 @@ +package com.alibou.security.auth.model; + +public enum TokenType { + BEARER +} diff --git a/src/main/java/com/alibou/security/token/TokenRepository.java b/src/main/java/com/alibou/security/auth/repository/TokenRepository.java similarity index 77% rename from src/main/java/com/alibou/security/token/TokenRepository.java rename to src/main/java/com/alibou/security/auth/repository/TokenRepository.java index 48235d8..a3710be 100644 --- a/src/main/java/com/alibou/security/token/TokenRepository.java +++ b/src/main/java/com/alibou/security/auth/repository/TokenRepository.java @@ -1,7 +1,9 @@ -package com.alibou.security.token; +package com.alibou.security.auth.repository; import java.util.List; import java.util.Optional; + +import com.alibou.security.auth.model.Token; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,7 +14,7 @@ public interface TokenRepository extends JpaRepository { on t.user.id = u.id\s where u.id = :id and (t.expired = false or t.revoked = false)\s """) - List findAllValidTokenByUser(Integer id); + List findAllValidTokenByUser(Long id); Optional findByToken(String token); } diff --git a/src/main/java/com/alibou/security/auditing/ApplicationAuditAware.java b/src/main/java/com/alibou/security/auth/service/ApplicationAuditAware.java similarity index 75% rename from src/main/java/com/alibou/security/auditing/ApplicationAuditAware.java rename to src/main/java/com/alibou/security/auth/service/ApplicationAuditAware.java index 3f8172f..68339d0 100644 --- a/src/main/java/com/alibou/security/auditing/ApplicationAuditAware.java +++ b/src/main/java/com/alibou/security/auth/service/ApplicationAuditAware.java @@ -1,6 +1,6 @@ -package com.alibou.security.auditing; +package com.alibou.security.auth.service; -import com.alibou.security.user.User; +import com.alibou.security.user.model.User; import org.springframework.data.domain.AuditorAware; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -8,15 +8,16 @@ import java.util.Optional; -public class ApplicationAuditAware implements AuditorAware { +public class ApplicationAuditAware implements AuditorAware { + @Override - public Optional getCurrentAuditor() { + public Optional getCurrentAuditor() { Authentication authentication = SecurityContextHolder .getContext() .getAuthentication(); if (authentication == null || - !authentication.isAuthenticated() || + !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken ) { return Optional.empty(); diff --git a/src/main/java/com/alibou/security/auth/service/AuthenticationService.java b/src/main/java/com/alibou/security/auth/service/AuthenticationService.java new file mode 100644 index 0000000..423da9c --- /dev/null +++ b/src/main/java/com/alibou/security/auth/service/AuthenticationService.java @@ -0,0 +1,126 @@ +package com.alibou.security.auth.service; + +import com.alibou.security.auth.model.RegisterRequest; +import com.alibou.security.auth.model.AuthenticationRequest; +import com.alibou.security.auth.model.AuthenticationResponse; +import com.alibou.security.config.JwtService; +import com.alibou.security.auth.model.Token; +import com.alibou.security.auth.repository.TokenRepository; +import com.alibou.security.auth.model.TokenType; +import com.alibou.security.user.model.User; +import com.alibou.security.user.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityExistsException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Optional; + +@Service +public class AuthenticationService { + private final UserRepository repository; + private final TokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + + public AuthenticationService(UserRepository repository, TokenRepository tokenRepository, PasswordEncoder passwordEncoder, JwtService jwtService, AuthenticationManager authenticationManager, UserRepository userRepository) { + this.repository = repository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + this.jwtService = jwtService; + this.authenticationManager = authenticationManager; + this.userRepository = userRepository; + } + + @NotNull + public AuthenticationResponse register(@NotNull RegisterRequest request) { + final Optional existUser = userRepository.findByEmail(request.email()); + if (existUser.isPresent()) { + final String errorMessage = String.format("User with given email = %s already exists", request.email()); + throw new EntityExistsException(errorMessage); + } + final User user = new User(); + user.setFirstname(request.firstname()); + user.setLastname(request.lastname()); + user.setEmail(request.email()); + user.setPassword(passwordEncoder.encode(request.password())); + user.setRole(request.role()); + var savedUser = repository.save(user); + var jwtToken = jwtService.generateToken(user); + var refreshToken = jwtService.generateRefreshToken(user); + saveUserToken(savedUser, jwtToken); + return new AuthenticationResponse(jwtToken, refreshToken); + } + + @NotNull + public AuthenticationResponse authenticate(@NotNull AuthenticationRequest request) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.email(), + request.password() + ) + ); + var user = repository.findByEmail(request.email()) + .orElseThrow(); + var jwtToken = jwtService.generateToken(user); + var refreshToken = jwtService.generateRefreshToken(user); + revokeAllUserTokens(user); + saveUserToken(user, jwtToken); + return new AuthenticationResponse(jwtToken, refreshToken); + } + + private void saveUserToken(@NotNull User user, @NotNull String jwtToken) { + final Token token = new Token(); + token.setUser(user); + token.setToken(jwtToken); + token.setTokenType(TokenType.BEARER); + token.setExpired(false); + token.setRevoked(false); + tokenRepository.save(token); + } + + private void revokeAllUserTokens(@NotNull User user) { + var validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId()); + if (validUserTokens.isEmpty()) + return; + validUserTokens.forEach(token -> { + token.setExpired(true); + token.setRevoked(true); + }); + tokenRepository.saveAll(validUserTokens); + } + + public void refreshToken(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response + ) throws IOException { + final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + final String refreshToken; + final String userEmail; + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return; + } + refreshToken = authHeader.substring(7); + userEmail = jwtService.extractUsername(refreshToken); + if (userEmail != null) { + var user = this.repository.findByEmail(userEmail) + .orElseThrow(); + if (jwtService.isTokenValid(refreshToken, user)) { + var accessToken = jwtService.generateToken(user); + revokeAllUserTokens(user); + saveUserToken(user, accessToken); + var authResponse = new AuthenticationResponse(accessToken, refreshToken); + new ObjectMapper().writeValue(response.getOutputStream(), authResponse); + } + } + } +} diff --git a/src/main/java/com/alibou/security/book/Book.java b/src/main/java/com/alibou/security/book/Book.java deleted file mode 100644 index 3f041af..0000000 --- a/src/main/java/com/alibou/security/book/Book.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.alibou.security.book; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -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 org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@EntityListeners(AuditingEntityListener.class) -public class Book { - - @Id - @GeneratedValue - private Integer id; - private String author; - private String isbn; - - @CreatedDate - @Column( - nullable = false, - updatable = false - ) - private LocalDateTime createDate; - - @LastModifiedDate - @Column(insertable = false) - private LocalDateTime lastModified; - - - @CreatedBy - @Column( - nullable = false, - updatable = false - ) - private Integer createdBy; - - @LastModifiedBy - @Column(insertable = false) - private Integer lastModifiedBy; -} diff --git a/src/main/java/com/alibou/security/book/BookController.java b/src/main/java/com/alibou/security/book/BookController.java deleted file mode 100644 index 4c45728..0000000 --- a/src/main/java/com/alibou/security/book/BookController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.alibou.security.book; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/books") -@RequiredArgsConstructor -public class BookController { - - private final BookService service; - - @PostMapping - public ResponseEntity save( - @RequestBody BookRequest request - ) { - service.save(request); - return ResponseEntity.accepted().build(); - } - - @GetMapping - public ResponseEntity> findAllBooks() { - return ResponseEntity.ok(service.findAll()); - } -} diff --git a/src/main/java/com/alibou/security/book/BookRepository.java b/src/main/java/com/alibou/security/book/BookRepository.java deleted file mode 100644 index 21ca467..0000000 --- a/src/main/java/com/alibou/security/book/BookRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.alibou.security.book; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface BookRepository extends JpaRepository { -} diff --git a/src/main/java/com/alibou/security/book/BookRequest.java b/src/main/java/com/alibou/security/book/BookRequest.java deleted file mode 100644 index dcf6765..0000000 --- a/src/main/java/com/alibou/security/book/BookRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.alibou.security.book; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Builder -public class BookRequest { - - private Integer id; - private String author; - private String isbn; -} diff --git a/src/main/java/com/alibou/security/book/BookService.java b/src/main/java/com/alibou/security/book/BookService.java deleted file mode 100644 index c09ded8..0000000 --- a/src/main/java/com/alibou/security/book/BookService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.alibou.security.book; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class BookService { - - private final BookRepository repository; - - public void save(BookRequest request) { - var book = Book.builder() - .id(request.getId()) - .author(request.getAuthor()) - .isbn(request.getIsbn()) - .build(); - repository.save(book); - } - - public List findAll() { - return repository.findAll(); - } -} diff --git a/src/main/java/com/alibou/security/config/ApplicationConfig.java b/src/main/java/com/alibou/security/config/ApplicationConfig.java index ae71abf..b911888 100644 --- a/src/main/java/com/alibou/security/config/ApplicationConfig.java +++ b/src/main/java/com/alibou/security/config/ApplicationConfig.java @@ -1,9 +1,7 @@ package com.alibou.security.config; -import com.alibou.security.auditing.ApplicationAuditAware; -import com.alibou.security.user.UserRepository; -import jakarta.persistence.criteria.CriteriaBuilder; -import lombok.RequiredArgsConstructor; +import com.alibou.security.auth.service.ApplicationAuditAware; +import com.alibou.security.user.repository.UserRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; @@ -17,38 +15,41 @@ import org.springframework.security.crypto.password.PasswordEncoder; @Configuration -@RequiredArgsConstructor public class ApplicationConfig { - private final UserRepository repository; - - @Bean - public UserDetailsService userDetailsService() { - return username -> repository.findByEmail(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - } - - @Bean - public AuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService()); - authProvider.setPasswordEncoder(passwordEncoder()); - return authProvider; - } - - @Bean - public AuditorAware auditorAware() { - return new ApplicationAuditAware(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + private final UserRepository repository; + + public ApplicationConfig(UserRepository repository) { + this.repository = repository; + } + + @Bean + public UserDetailsService userDetailsService() { + return username -> repository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuditorAware auditorAware() { + return new ApplicationAuditAware(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/src/main/java/com/alibou/security/config/GlobalExceptionHandler.java b/src/main/java/com/alibou/security/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..3f3bb20 --- /dev/null +++ b/src/main/java/com/alibou/security/config/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package com.alibou.security.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + final ErrorResponse errorDetails = ErrorResponse.create(ex, HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/src/main/java/com/alibou/security/config/JwtAuthenticationFilter.java b/src/main/java/com/alibou/security/config/JwtAuthenticationFilter.java index d6e55d1..143c4a2 100644 --- a/src/main/java/com/alibou/security/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/alibou/security/config/JwtAuthenticationFilter.java @@ -1,18 +1,13 @@ package com.alibou.security.config; -import com.alibou.security.token.TokenRepository; +import com.alibou.security.auth.repository.TokenRepository; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.beans.Transient; import java.io.IOException; -import java.security.Security; -import jakarta.transaction.TransactionScoped; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -23,49 +18,56 @@ import org.springframework.web.filter.OncePerRequestFilter; @Component -@RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; - private final UserDetailsService userDetailsService; - private final TokenRepository tokenRepository; + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + private final TokenRepository tokenRepository; - @Override - protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain - ) throws ServletException, IOException { - if (request.getServletPath().contains("/api/v1/auth")) { - filterChain.doFilter(request, response); - return; + public JwtAuthenticationFilter(JwtService jwtService, + UserDetailsService userDetailsService, + TokenRepository tokenRepository) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + this.tokenRepository = tokenRepository; } - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String userEmail; - if (authHeader == null ||!authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - jwt = authHeader.substring(7); - userEmail = jwtService.extractUsername(jwt); - if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - var isTokenValid = tokenRepository.findByToken(jwt) - .map(t -> !t.isExpired() && !t.isRevoked()) - .orElse(false); - if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + if (request.getServletPath().contains("/api/v1/auth")) { + filterChain.doFilter(request, response); + return; + } + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String userEmail; + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + jwt = authHeader.substring(7); + userEmail = jwtService.extractUsername(jwt); + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + var isTokenValid = tokenRepository.findByToken(jwt) + .map(t -> !t.isExpired() && !t.isRevoked()) + .orElse(false); + if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); } - filterChain.doFilter(request, response); - } } diff --git a/src/main/java/com/alibou/security/config/LogoutService.java b/src/main/java/com/alibou/security/config/LogoutService.java index 0784565..932cc3c 100644 --- a/src/main/java/com/alibou/security/config/LogoutService.java +++ b/src/main/java/com/alibou/security/config/LogoutService.java @@ -1,39 +1,41 @@ package com.alibou.security.config; -import com.alibou.security.token.TokenRepository; +import com.alibou.security.auth.repository.TokenRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Service; @Service -@RequiredArgsConstructor public class LogoutService implements LogoutHandler { - private final TokenRepository tokenRepository; + private final TokenRepository tokenRepository; - @Override - public void logout( - HttpServletRequest request, - HttpServletResponse response, - Authentication authentication - ) { - final String authHeader = request.getHeader("Authorization"); - final String jwt; - if (authHeader == null ||!authHeader.startsWith("Bearer ")) { - return; + public LogoutService(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; } - jwt = authHeader.substring(7); - var storedToken = tokenRepository.findByToken(jwt) - .orElse(null); - if (storedToken != null) { - storedToken.setExpired(true); - storedToken.setRevoked(true); - tokenRepository.save(storedToken); - SecurityContextHolder.clearContext(); + + @Override + public void logout( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return; + } + jwt = authHeader.substring(7); + var storedToken = tokenRepository.findByToken(jwt) + .orElse(null); + if (storedToken != null) { + storedToken.setExpired(true); + storedToken.setRevoked(true); + tokenRepository.save(storedToken); + SecurityContextHolder.clearContext(); + } } - } } diff --git a/src/main/java/com/alibou/security/config/SecurityConfiguration.java b/src/main/java/com/alibou/security/config/SecurityConfiguration.java index e4aefe6..1ca2cd7 100644 --- a/src/main/java/com/alibou/security/config/SecurityConfiguration.java +++ b/src/main/java/com/alibou/security/config/SecurityConfiguration.java @@ -1,6 +1,5 @@ package com.alibou.security.config; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; @@ -13,16 +12,12 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; -import static com.alibou.security.user.Permission.ADMIN_CREATE; -import static com.alibou.security.user.Permission.ADMIN_DELETE; -import static com.alibou.security.user.Permission.ADMIN_READ; -import static com.alibou.security.user.Permission.ADMIN_UPDATE; -import static com.alibou.security.user.Permission.MANAGER_CREATE; -import static com.alibou.security.user.Permission.MANAGER_DELETE; -import static com.alibou.security.user.Permission.MANAGER_READ; -import static com.alibou.security.user.Permission.MANAGER_UPDATE; -import static com.alibou.security.user.Role.ADMIN; -import static com.alibou.security.user.Role.MANAGER; +import static com.alibou.security.user.model.Permission.ADMIN_CREATE; +import static com.alibou.security.user.model.Permission.ADMIN_DELETE; +import static com.alibou.security.user.model.Permission.ADMIN_READ; +import static com.alibou.security.user.model.Permission.ADMIN_UPDATE; + +import static com.alibou.security.user.model.Role.ADMIN; import static org.springframework.http.HttpMethod.DELETE; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -31,7 +26,6 @@ @Configuration @EnableWebSecurity -@RequiredArgsConstructor @EnableMethodSecurity public class SecurityConfiguration { @@ -50,6 +44,14 @@ public class SecurityConfiguration { private final AuthenticationProvider authenticationProvider; private final LogoutHandler logoutHandler; + public SecurityConfiguration(JwtAuthenticationFilter jwtAuthFilter, + AuthenticationProvider authenticationProvider, + LogoutHandler logoutHandler) { + this.jwtAuthFilter = jwtAuthFilter; + this.authenticationProvider = authenticationProvider; + this.logoutHandler = logoutHandler; + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -57,11 +59,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(req -> req.requestMatchers(WHITE_LIST_URL) .permitAll() - .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name()) - .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name()) - .requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name()) - .requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name()) - .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name()) + .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name()) + .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name()) + .requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name()) + .requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name()) + .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name()) .anyRequest() .authenticated() ) diff --git a/src/main/java/com/alibou/security/demo/AdminController.java b/src/main/java/com/alibou/security/demo/AdminController.java deleted file mode 100644 index 18ede65..0000000 --- a/src/main/java/com/alibou/security/demo/AdminController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.alibou.security.demo; - -import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/admin") -@PreAuthorize("hasRole('ADMIN')") -public class AdminController { - - @GetMapping - @PreAuthorize("hasAuthority('admin:read')") - public String get() { - return "GET:: admin controller"; - } - @PostMapping - @PreAuthorize("hasAuthority('admin:create')") - @Hidden - public String post() { - return "POST:: admin controller"; - } - @PutMapping - @PreAuthorize("hasAuthority('admin:update')") - @Hidden - public String put() { - return "PUT:: admin controller"; - } - @DeleteMapping - @PreAuthorize("hasAuthority('admin:delete')") - @Hidden - public String delete() { - return "DELETE:: admin controller"; - } -} diff --git a/src/main/java/com/alibou/security/demo/DemoController.java b/src/main/java/com/alibou/security/demo/DemoController.java deleted file mode 100644 index ee2c380..0000000 --- a/src/main/java/com/alibou/security/demo/DemoController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.alibou.security.demo; - -import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/demo-controller") -@Hidden -public class DemoController { - - @GetMapping - public ResponseEntity sayHello() { - return ResponseEntity.ok("Hello from secured endpoint"); - } - -} diff --git a/src/main/java/com/alibou/security/demo/ManagementController.java b/src/main/java/com/alibou/security/demo/ManagementController.java deleted file mode 100644 index a214a9b..0000000 --- a/src/main/java/com/alibou/security/demo/ManagementController.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.alibou.security.demo; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/management") -@Tag(name = "Management") -public class ManagementController { - - - @Operation( - description = "Get endpoint for manager", - summary = "This is a summary for management get endpoint", - responses = { - @ApiResponse( - description = "Success", - responseCode = "200" - ), - @ApiResponse( - description = "Unauthorized / Invalid Token", - responseCode = "403" - ) - } - - ) - @GetMapping - public String get() { - return "GET:: management controller"; - } - @PostMapping - public String post() { - return "POST:: management controller"; - } - @PutMapping - public String put() { - return "PUT:: management controller"; - } - @DeleteMapping - public String delete() { - return "DELETE:: management controller"; - } -} diff --git a/src/main/java/com/alibou/security/token/Token.java b/src/main/java/com/alibou/security/token/Token.java deleted file mode 100644 index 71f3571..0000000 --- a/src/main/java/com/alibou/security/token/Token.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.alibou.security.token; - -import com.alibou.security.user.User; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -public class Token { - - @Id - @GeneratedValue - public Integer id; - - @Column(unique = true) - public String token; - - @Enumerated(EnumType.STRING) - public TokenType tokenType = TokenType.BEARER; - - public boolean revoked; - - public boolean expired; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - public User user; -} diff --git a/src/main/java/com/alibou/security/token/TokenType.java b/src/main/java/com/alibou/security/token/TokenType.java deleted file mode 100644 index 82a8cff..0000000 --- a/src/main/java/com/alibou/security/token/TokenType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.alibou.security.token; - -public enum TokenType { - BEARER -} diff --git a/src/main/java/com/alibou/security/user/ChangePasswordRequest.java b/src/main/java/com/alibou/security/user/ChangePasswordRequest.java deleted file mode 100644 index 70bca36..0000000 --- a/src/main/java/com/alibou/security/user/ChangePasswordRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.alibou.security.user; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Builder -public class ChangePasswordRequest { - - private String currentPassword; - private String newPassword; - private String confirmationPassword; -} diff --git a/src/main/java/com/alibou/security/user/Permission.java b/src/main/java/com/alibou/security/user/Permission.java deleted file mode 100644 index 16ae8b4..0000000 --- a/src/main/java/com/alibou/security/user/Permission.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alibou.security.user; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum Permission { - - ADMIN_READ("admin:read"), - ADMIN_UPDATE("admin:update"), - ADMIN_CREATE("admin:create"), - ADMIN_DELETE("admin:delete"), - MANAGER_READ("management:read"), - MANAGER_UPDATE("management:update"), - MANAGER_CREATE("management:create"), - MANAGER_DELETE("management:delete") - - ; - - @Getter - private final String permission; -} diff --git a/src/main/java/com/alibou/security/user/Role.java b/src/main/java/com/alibou/security/user/Role.java deleted file mode 100644 index 0ff9bd1..0000000 --- a/src/main/java/com/alibou/security/user/Role.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.alibou.security.user; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import static com.alibou.security.user.Permission.ADMIN_CREATE; -import static com.alibou.security.user.Permission.ADMIN_DELETE; -import static com.alibou.security.user.Permission.ADMIN_READ; -import static com.alibou.security.user.Permission.ADMIN_UPDATE; -import static com.alibou.security.user.Permission.MANAGER_CREATE; -import static com.alibou.security.user.Permission.MANAGER_DELETE; -import static com.alibou.security.user.Permission.MANAGER_READ; -import static com.alibou.security.user.Permission.MANAGER_UPDATE; - -@RequiredArgsConstructor -public enum Role { - - USER(Collections.emptySet()), - ADMIN( - Set.of( - ADMIN_READ, - ADMIN_UPDATE, - ADMIN_DELETE, - ADMIN_CREATE, - MANAGER_READ, - MANAGER_UPDATE, - MANAGER_DELETE, - MANAGER_CREATE - ) - ), - MANAGER( - Set.of( - MANAGER_READ, - MANAGER_UPDATE, - MANAGER_DELETE, - MANAGER_CREATE - ) - ) - - ; - - @Getter - private final Set permissions; - - public List getAuthorities() { - var authorities = getPermissions() - .stream() - .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) - .collect(Collectors.toList()); - authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name())); - return authorities; - } -} diff --git a/src/main/java/com/alibou/security/user/User.java b/src/main/java/com/alibou/security/user/User.java deleted file mode 100644 index bc4e086..0000000 --- a/src/main/java/com/alibou/security/user/User.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.alibou.security.user; - -import com.alibou.security.token.Token; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import java.util.Collection; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "_user") -public class User implements UserDetails { - - @Id - @GeneratedValue - private Integer id; - private String firstname; - private String lastname; - private String email; - private String password; - - @Enumerated(EnumType.STRING) - private Role role; - - @OneToMany(mappedBy = "user") - private List tokens; - - @Override - public Collection getAuthorities() { - return role.getAuthorities(); - } - - @Override - public String getPassword() { - return password; - } - - @Override - public String getUsername() { - return email; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/com/alibou/security/user/UserService.java b/src/main/java/com/alibou/security/user/UserService.java deleted file mode 100644 index a17181d..0000000 --- a/src/main/java/com/alibou/security/user/UserService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.alibou.security.user; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.security.Principal; - -@Service -@RequiredArgsConstructor -public class UserService { - - private final PasswordEncoder passwordEncoder; - private final UserRepository repository; - public void changePassword(ChangePasswordRequest request, Principal connectedUser) { - - var user = (User) ((UsernamePasswordAuthenticationToken) connectedUser).getPrincipal(); - - // check if the current password is correct - if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) { - throw new IllegalStateException("Wrong password"); - } - // check if the two new passwords are the same - if (!request.getNewPassword().equals(request.getConfirmationPassword())) { - throw new IllegalStateException("Password are not the same"); - } - - // update the password - user.setPassword(passwordEncoder.encode(request.getNewPassword())); - - // save the new password - repository.save(user); - } -} diff --git a/src/main/java/com/alibou/security/user/UserController.java b/src/main/java/com/alibou/security/user/controller/UserController.java similarity index 74% rename from src/main/java/com/alibou/security/user/UserController.java rename to src/main/java/com/alibou/security/user/controller/UserController.java index 415be48..49bc961 100644 --- a/src/main/java/com/alibou/security/user/UserController.java +++ b/src/main/java/com/alibou/security/user/controller/UserController.java @@ -1,6 +1,7 @@ -package com.alibou.security.user; +package com.alibou.security.user.controller; -import lombok.RequiredArgsConstructor; +import com.alibou.security.user.service.UserService; +import com.alibou.security.user.model.ChangePasswordRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -11,11 +12,14 @@ @RestController @RequestMapping("/api/v1/users") -@RequiredArgsConstructor public class UserController { private final UserService service; + public UserController(UserService service) { + this.service = service; + } + @PatchMapping public ResponseEntity changePassword( @RequestBody ChangePasswordRequest request, diff --git a/src/main/java/com/alibou/security/user/model/ChangePasswordRequest.java b/src/main/java/com/alibou/security/user/model/ChangePasswordRequest.java new file mode 100644 index 0000000..0097f66 --- /dev/null +++ b/src/main/java/com/alibou/security/user/model/ChangePasswordRequest.java @@ -0,0 +1,9 @@ +package com.alibou.security.user.model; + + +import org.jetbrains.annotations.NotNull; + +public record ChangePasswordRequest(@NotNull String currentPassword, + @NotNull String newPassword, + @NotNull String confirmationPassword) { +} diff --git a/src/main/java/com/alibou/security/user/model/Permission.java b/src/main/java/com/alibou/security/user/model/Permission.java new file mode 100644 index 0000000..3602a7f --- /dev/null +++ b/src/main/java/com/alibou/security/user/model/Permission.java @@ -0,0 +1,19 @@ +package com.alibou.security.user.model; + +public enum Permission { + + ADMIN_READ("admin:read"), + ADMIN_UPDATE("admin:update"), + ADMIN_CREATE("admin:create"), + ADMIN_DELETE("admin:delete"); + + private final String permission; + + Permission(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} diff --git a/src/main/java/com/alibou/security/user/model/Role.java b/src/main/java/com/alibou/security/user/model/Role.java new file mode 100644 index 0000000..86b3443 --- /dev/null +++ b/src/main/java/com/alibou/security/user/model/Role.java @@ -0,0 +1,41 @@ +package com.alibou.security.user.model; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.alibou.security.user.model.Permission.ADMIN_CREATE; +import static com.alibou.security.user.model.Permission.ADMIN_DELETE; +import static com.alibou.security.user.model.Permission.ADMIN_READ; +import static com.alibou.security.user.model.Permission.ADMIN_UPDATE; + +public enum Role { + + USER(Collections.emptySet()), + ADMIN(Set.of(ADMIN_READ, + ADMIN_UPDATE, + ADMIN_DELETE, + ADMIN_CREATE)); + + private final Set permissions; + + Role(Set permissions) { + this.permissions = permissions; + } + + public List getAuthorities() { + var authorities = getPermissions() + .stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toList()); + authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name())); + return authorities; + } + + public Set getPermissions() { + return permissions; + } +} diff --git a/src/main/java/com/alibou/security/user/model/User.java b/src/main/java/com/alibou/security/user/model/User.java new file mode 100644 index 0000000..1c64404 --- /dev/null +++ b/src/main/java/com/alibou/security/user/model/User.java @@ -0,0 +1,136 @@ +package com.alibou.security.user.model; + +import com.alibou.security.auth.model.Token; +import jakarta.persistence.*; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.hibernate.annotations.GenericGenerator; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Entity +@Table(name = "_user") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native") + private Long id; + + private String firstname; + + private String lastname; + + private String email; + + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + @OneToMany(mappedBy = "user") + private List tokens; + + @Override + public Collection getAuthorities() { + return role.getAuthorities(); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public List getTokens() { + return tokens; + } + + public void setTokens(List tokens) { + this.tokens = tokens; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id) && Objects.equals(firstname, user.firstname) && Objects.equals(lastname, user.lastname) && Objects.equals(email, user.email) && Objects.equals(password, user.password) && role == user.role && Objects.equals(tokens, user.tokens); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstname, lastname, email, password, role, tokens); + } +} diff --git a/src/main/java/com/alibou/security/user/UserRepository.java b/src/main/java/com/alibou/security/user/repository/UserRepository.java similarity index 70% rename from src/main/java/com/alibou/security/user/UserRepository.java rename to src/main/java/com/alibou/security/user/repository/UserRepository.java index a979ad6..403ccbd 100644 --- a/src/main/java/com/alibou/security/user/UserRepository.java +++ b/src/main/java/com/alibou/security/user/repository/UserRepository.java @@ -1,6 +1,8 @@ -package com.alibou.security.user; +package com.alibou.security.user.repository; import java.util.Optional; + +import com.alibou.security.user.model.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { diff --git a/src/main/java/com/alibou/security/user/service/UserService.java b/src/main/java/com/alibou/security/user/service/UserService.java new file mode 100644 index 0000000..d3f02ca --- /dev/null +++ b/src/main/java/com/alibou/security/user/service/UserService.java @@ -0,0 +1,10 @@ +package com.alibou.security.user.service; + +import com.alibou.security.user.model.ChangePasswordRequest; +import org.jetbrains.annotations.NotNull; + +import java.security.Principal; + +public interface UserService { + void changePassword(@NotNull ChangePasswordRequest request, @NotNull Principal connectedUser); +} diff --git a/src/main/java/com/alibou/security/user/service/impl/UserServiceImpl.java b/src/main/java/com/alibou/security/user/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..7f3b8ad --- /dev/null +++ b/src/main/java/com/alibou/security/user/service/impl/UserServiceImpl.java @@ -0,0 +1,46 @@ +package com.alibou.security.user.service.impl; + +import com.alibou.security.user.model.ChangePasswordRequest; +import com.alibou.security.user.model.User; +import com.alibou.security.user.repository.UserRepository; +import com.alibou.security.user.service.UserService; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.security.Principal; + +@Service +public class UserServiceImpl implements UserService { + + private final PasswordEncoder passwordEncoder; + private final UserRepository repository; + + public UserServiceImpl(PasswordEncoder passwordEncoder, + UserRepository repository) { + this.passwordEncoder = passwordEncoder; + this.repository = repository; + } + + @Override + public void changePassword(@NotNull ChangePasswordRequest request, @NotNull Principal connectedUser) { + + var user = (User) ((UsernamePasswordAuthenticationToken) connectedUser).getPrincipal(); + + // check if the current password is correct + if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) { + throw new IllegalStateException("Wrong password"); + } + // check if the two new passwords are the same + if (!request.newPassword().equals(request.confirmationPassword())) { + throw new IllegalStateException("Password are not the same"); + } + + // update the password + user.setPassword(passwordEncoder.encode(request.newPassword())); + + // save the new password + repository.save(user); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 71b71d1..e060b4d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,18 +1,19 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/jwt_security - username: username - password: password - driver-class-name: org.postgresql.Driver + url: jdbc:mysql://localhost:3306/warehouse + username: root + password: Mert121217 + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: hibernate: - ddl-auto: create-drop - show-sql: false + ddl-auto: none + show-sql: true properties: hibernate: format_sql: true - database: postgresql - database-platform: org.hibernate.dialect.PostgreSQLDialect + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect application: security: