From 493d7ad1762f87786012e697a8e936ae2f682751 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Fri, 2 Feb 2024 12:55:46 +0100 Subject: [PATCH 01/45] chore: removed .idea --- .gitignore | 3 ++- .idea/.gitignore | 3 --- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ .idea/wiq_en2b.iml | 9 --------- 6 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/wiq_en2b.iml diff --git a/.gitignore b/.gitignore index 8bbe72a8..613d1130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules coverage -docs/build \ No newline at end of file +docs/build +.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 07115cdf..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5668832c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/wiq_en2b.iml b/.idea/wiq_en2b.iml deleted file mode 100644 index d6ebd480..00000000 --- a/.idea/wiq_en2b.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From 500baa59b8e283960dd70876b16d0cb649b93686 Mon Sep 17 00:00:00 2001 From: Gonzalo Alonso Fernandez Date: Fri, 2 Feb 2024 17:48:10 +0100 Subject: [PATCH 02/45] chore: Add my name to the readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5b42a0b5..950ef4c0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). +## Contributors +Gonzalo Alonso Fernández - UO282104 + This repo is a basic application composed of several components. - **Gateway service**. Express service that is exposed to the public and serves as a proxy to the two previous ones. From dd129f1fc28046aad6891b52eaac864f3a5ba92e Mon Sep 17 00:00:00 2001 From: Sergio Rodriguez Date: Fri, 2 Feb 2024 18:03:32 +0100 Subject: [PATCH 03/45] chore: Add my name to the readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5b42a0b5..bbf07c90 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). +## Contributors +Gonzalo Alonso Fernández - UO282104 +Sergio Rodríguez García - UO282598 + This repo is a basic application composed of several components. - **Gateway service**. Express service that is exposed to the public and serves as a proxy to the two previous ones. From ca17abed580b532f300db31e1d3b56a39867e44c Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Fri, 2 Feb 2024 18:20:57 +0100 Subject: [PATCH 04/45] chore: add my name to the readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bbf07c90..d29edf4a 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,11 @@ This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). ## Contributors -Gonzalo Alonso Fernández - UO282104 -Sergio Rodríguez García - UO282598 +| Nombre | UO | +|--------|----| +| Gonzalo Alonso Fernández | UO282104 | +| Sergio Rodríguez García | UO282598 | +| Jorge Joaquín Gancedo Fernández | UO282161 | This repo is a basic application composed of several components. From 459f778342d4e0a8eff85e17218c86e82b376eda Mon Sep 17 00:00:00 2001 From: UO288090 Date: Sat, 3 Feb 2024 17:06:13 +0100 Subject: [PATCH 05/45] chore: add name to contributors in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d29edf4a..f09fd34b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This is a base repo for the [Software Architecture course](http://arquisoft.gith | Gonzalo Alonso Fernández | UO282104 | | Sergio Rodríguez García | UO282598 | | Jorge Joaquín Gancedo Fernández | UO282161 | +| Sergio Quintana Fernández | UO288090 | This repo is a basic application composed of several components. From efd0dfe48f8b4c6708e08c6469bd28ed6a8f7a24 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 19:51:48 +0100 Subject: [PATCH 06/45] chore: name in README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f09fd34b..34fd9828 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,13 @@ This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). ## Contributors -| Nombre | UO | -|--------|----| -| Gonzalo Alonso Fernández | UO282104 | -| Sergio Rodríguez García | UO282598 | +| Nombre | UO | +|---------------------------------|----------| +| Gonzalo Alonso Fernández | UO282104 | +| Darío Gutiérrez Mori | UO282435 | +| Sergio Rodríguez García | UO282598 | | Jorge Joaquín Gancedo Fernández | UO282161 | -| Sergio Quintana Fernández | UO288090 | +| Sergio Quintana Fernández | UO288090 | This repo is a basic application composed of several components. From 6a2d1243379e3ad5f4aa79ca3518d3a735537e88 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:14:49 +0100 Subject: [PATCH 07/45] feat: auth controller --- api/pom.xml | 6 +++++ .../lab/en2b/quizapi/auth/AuthController.java | 22 +++++++++++++++++++ .../lab/en2b/quizapi/auth/AuthService.java | 14 ++++++++++++ .../lab/en2b/quizapi/auth/dtos/LoginDto.java | 13 +++++++++++ 4 files changed, 55 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/AuthController.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/AuthService.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java diff --git a/api/pom.xml b/api/pom.xml index 70e9a137..a12a6ded 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,6 +20,12 @@ org.springframework.boot spring-boot-starter-data-jpa + 3.1.4 + + + jakarta.validation + jakarta.validation-api + 3.0.2 org.springframework.boot diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java new file mode 100644 index 00000000..d94b4c0f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -0,0 +1,22 @@ +package lab.en2b.quizapi.auth; + +import jakarta.validation.Valid; +import lab.en2b.quizapi.auth.dtos.LoginDto; +import org.springframework.beans.factory.annotation.Autowired; +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; +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ + return authService.login(loginRequest); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java new file mode 100644 index 00000000..5f14fd4a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.auth; + +import lab.en2b.quizapi.auth.dtos.LoginDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + public ResponseEntity login(LoginDto loginRequest) { + throw new UnsupportedOperationException(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java new file mode 100644 index 00000000..1f0eecf0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.auth.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class LoginDto { + private String email; + private String password; +} From f5b77914a599e42d9f2be6c56aa3025385bd7c78 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:17:09 +0100 Subject: [PATCH 08/45] feat: register endpoint --- .../lab/en2b/quizapi/auth/AuthController.java | 6 ++++++ .../lab/en2b/quizapi/auth/AuthService.java | 5 +++++ .../en2b/quizapi/auth/dtos/RegisterDto.java | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index d94b4c0f..3ce5da36 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -15,6 +16,11 @@ public class AuthController { @Autowired private AuthService authService; + @PostMapping("/register") + public ResponseEntity registerUser(@Valid @RequestBody RegisterDto registerRequest){ + return authService.register(registerRequest); + } + @PostMapping("/login") public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ return authService.login(loginRequest); diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 5f14fd4a..0d46fe38 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -11,4 +12,8 @@ public class AuthService { public ResponseEntity login(LoginDto loginRequest) { throw new UnsupportedOperationException(); } + + public ResponseEntity register(RegisterDto registerRequest) { + throw new UnsupportedOperationException(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java new file mode 100644 index 00000000..389b8583 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java @@ -0,0 +1,18 @@ +package lab.en2b.quizapi.auth.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RegisterDto { + @NonNull + private String email; + @NonNull + private String username; + @NonNull + private String password; +} From 2126f72ba3fe78367aef3e35550293ee52b56591 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:17:37 +0100 Subject: [PATCH 09/45] feat: non null checks in LoginDto --- api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java index 1f0eecf0..5e7a0ec5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java @@ -3,11 +3,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.NonNull; @NoArgsConstructor @AllArgsConstructor @Data public class LoginDto { + @NonNull private String email; + @NonNull private String password; } From 51e37fee67f2283b3a0745dfbcc459fdf9332172 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:22:28 +0100 Subject: [PATCH 10/45] feat: refresh token endpoint --- api/pom.xml | 5 +++++ .../lab/en2b/quizapi/auth/AuthController.java | 6 ++++++ .../java/lab/en2b/quizapi/auth/AuthService.java | 5 +++++ .../en2b/quizapi/auth/dtos/RefreshTokenDto.java | 16 ++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java diff --git a/api/pom.xml b/api/pom.xml index a12a6ded..e813a6d1 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -52,6 +52,11 @@ spring-security-test test + + com.fasterxml.jackson.core + jackson-annotations + 2.15.2 + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index 3ce5da36..dd755545 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -25,4 +26,9 @@ public ResponseEntity registerUser(@Valid @RequestBody RegisterDto registerRe public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ return authService.login(loginRequest); } + + @PostMapping("/refresh-token") + public ResponseEntity refreshToken(@Valid RefreshTokenDto refreshTokenRequest){ + return authService.refreshToken(refreshTokenRequest); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 0d46fe38..01067f0f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -16,4 +17,8 @@ public ResponseEntity login(LoginDto loginRequest) { public ResponseEntity register(RegisterDto registerRequest) { throw new UnsupportedOperationException(); } + + public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { + throw new UnsupportedOperationException(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java new file mode 100644 index 00000000..f327ae8a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java @@ -0,0 +1,16 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RefreshTokenDto { + @NonNull + @JsonProperty("refresh_token") + private String refreshToken; +} From d9376bea81fe76fb1c04bbe31a4025b9af7af836 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 20:49:08 +0100 Subject: [PATCH 11/45] feat: mocked security config --- api/pom.xml | 20 ++++++ .../lab/en2b/quizapi/auth/UserService.java | 14 +++++ .../quizapi/auth/config/SecurityConfig.java | 61 +++++++++++++++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 46 ++++++++++++++ .../lab/en2b/quizapi/auth/jwt/JwtService.java | 15 +++++ 5 files changed, 156 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/UserService.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java diff --git a/api/pom.xml b/api/pom.xml index e813a6d1..bba945c2 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -57,6 +57,26 @@ jackson-annotations 2.15.2 + + io.jsonwebtoken + jjwt-api + 0.12.1 + + + io.jsonwebtoken + jjwt-impl + 0.12.1 + + + io.jsonwebtoken + jjwt-jackson + 0.12.1 + + + org.apache.tomcat.embed + tomcat-embed-core + 10.1.13 + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java b/api/src/main/java/lab/en2b/quizapi/auth/UserService.java new file mode 100644 index 00000000..883c04e5 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/UserService.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.auth; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserService implements UserDetailsService { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + throw new UnsupportedOperationException(); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java new file mode 100644 index 00000000..e77b3f6a --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -0,0 +1,61 @@ +package lab.en2b.quizapi.auth.config; + +import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthFilter authFilter; + @Autowired + public UserService userService; + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); + userPasswordProvider.setUserDetailsService(userService); + userPasswordProvider.setPasswordEncoder(passwordEncoder()); + authenticationManagerBuilder.authenticationProvider(userPasswordProvider); + return authenticationManagerBuilder.build(); + } + /** + * Security filter used for filtering all petitions, applying cors and asking for authentication among other things + * @param http the http request to filter + * @return the filtered request + * @throws Exception if any problem happens when filtering + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authorize -> + authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() + .anyRequest().authenticated()) + .cors(Customizer.withDefaults()) + //TODO: add exception handling + .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); + } + + +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java new file mode 100644 index 00000000..d9b0369f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -0,0 +1,46 @@ +package lab.en2b.quizapi.auth.jwt; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lab.en2b.quizapi.auth.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + @Autowired + private JwtService jwtService; + + @Autowired + private UserService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + String token = null; + String username = null; + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + username = jwtService.extractUsername(token); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (jwtService.validateToken(token, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} + diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java new file mode 100644 index 00000000..a2c6c76b --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -0,0 +1,15 @@ +package lab.en2b.quizapi.auth.jwt; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class JwtService { + public String extractUsername(String token) { + throw new UnsupportedOperationException(); + } + + public boolean validateToken(String token, UserDetails userDetails) { + throw new UnsupportedOperationException(); + } +} From c1076831a76e60a22bfd56de8be850c4806a0ab1 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:01:59 +0100 Subject: [PATCH 12/45] feat: user and role tables --- .../quizapi/auth/config/SecurityConfig.java | 2 +- .../quizapi/auth/config/UserDetailsImpl.java | 62 ++++++++++++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 2 +- .../lab/en2b/quizapi/auth/jwt/JwtService.java | 4 +- .../main/java/lab/en2b/quizapi/user/User.java | 72 +++++++++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 11 +++ .../quizapi/{auth => user}/UserService.java | 8 ++- .../java/lab/en2b/quizapi/user/role/Role.java | 29 ++++++++ 8 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java create mode 100644 api/src/main/java/lab/en2b/quizapi/user/User.java create mode 100644 api/src/main/java/lab/en2b/quizapi/user/UserRepository.java rename api/src/main/java/lab/en2b/quizapi/{auth => user}/UserService.java (60%) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/role/Role.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index e77b3f6a..33c11b65 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.auth.config; -import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.user.UserService; import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java new file mode 100644 index 00000000..527a52e0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -0,0 +1,62 @@ +package lab.en2b.quizapi.auth.config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lab.en2b.quizapi.user.User; +import lab.en2b.quizapi.user.role.Role; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +@Getter +@AllArgsConstructor +public class UserDetailsImpl implements UserDetails { + @Serial + private static final long serialVersionUID = 1L; + private Long id; + private String username; + private String email; + @JsonIgnore + private String password; + private Collection authorities; + public static UserDetailsImpl build(User user) { + List authorities = new ArrayList<>(); + for(Role role : user.getRoles()){ + authorities.add(new SimpleGrantedAuthority(role.getName())); + } + return new UserDetailsImpl(user.getId(),user.getName() , user.getEmail(), user.getPassword(), authorities); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + @Override + public boolean isAccountNonLocked() { + return true; + } + @Override + public boolean isCredentialsNonExpired() { + return true; + } + @Override + public boolean isEnabled() { + return true; + } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d9b0369f..5ca1d5ed 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -3,7 +3,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lab.en2b.quizapi.auth.UserService; +import lab.en2b.quizapi.user.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a2c6c76b..3ec38b06 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -1,9 +1,9 @@ package lab.en2b.quizapi.auth.jwt; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -@Service +@Component public class JwtService { public String extractUsername(String token) { throw new UnsupportedOperationException(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java new file mode 100644 index 00000000..6b3bec08 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -0,0 +1,72 @@ +package lab.en2b.quizapi.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.user.role.Role; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Set; + +@Entity +@Table( name = "users", + uniqueConstraints = { + @UniqueConstraint(columnNames = "email"), + @UniqueConstraint(columnNames = "name") + }) +@RequiredArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@AllArgsConstructor +@Builder +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Setter(AccessLevel.NONE) + private Long id; + + @NotBlank + @Size(max=255) + @NonNull + private String name; + + @NotBlank + @Size(max = 255) + @Email + @NonNull + private String email; + + @NotBlank + @Size(max = 255) + @NonNull + private String password; + + @Column(name = "refresh_token",unique = true) + @Size(max = 255) + private String refreshToken; + + @Column(name="refresh_expiration") + private Instant refreshExpiration; + + @NotNull + @ManyToMany + @JoinTable(name="users_roles", + joinColumns= + @JoinColumn(name="user_id", referencedColumnName="id"), + inverseJoinColumns= + @JoinColumn(name="role_id", referencedColumnName="id") + ) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "permissions"}) + @JsonProperty("role") + private Set roles; +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java new file mode 100644 index 00000000..1e371aff --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -0,0 +1,11 @@ +package lab.en2b.quizapi.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java similarity index 60% rename from api/src/main/java/lab/en2b/quizapi/auth/UserService.java rename to api/src/main/java/lab/en2b/quizapi/user/UserService.java index 883c04e5..c387c7aa 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,14 +1,18 @@ -package lab.en2b.quizapi.auth; +package lab.en2b.quizapi.user; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class UserService implements UserDetailsService { + private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - throw new UnsupportedOperationException(); + return UserDetailsImpl.build(userRepository.findByUsername(username).orElseThrow()); } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java new file mode 100644 index 00000000..a3c58cd0 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -0,0 +1,29 @@ +package lab.en2b.quizapi.user.role; + +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Table(name = "roles") +@NoArgsConstructor +@RequiredArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@Builder +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Setter(AccessLevel.NONE) + private Long id; + + @NonNull + @NotBlank + @Size(max=255) + private String name; +} From 575f170b8a915c3acc67ebcd06a63bdba19bd898 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:18:14 +0100 Subject: [PATCH 13/45] feat: jwt auth filter --- .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 16 ++-- .../lab/en2b/quizapi/auth/jwt/JwtService.java | 85 ++++++++++++++++++- .../lab/en2b/quizapi/user/UserRepository.java | 2 +- .../lab/en2b/quizapi/user/UserService.java | 2 +- .../java/lab/en2b/quizapi/user/role/Role.java | 3 - 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 5ca1d5ed..8806cbc1 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lab.en2b.quizapi.user.UserService; +import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -23,24 +24,23 @@ public class JwtAuthFilter extends OncePerRequestFilter { private UserService userDetailsService; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null; if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); - username = jwtService.extractUsername(token); + username = jwtService.getSubjectFromJwtToken(token); } - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && jwtService.validateJwtToken(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - if (jwtService.validateToken(token, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); } filterChain.doFilter(request, response); } + } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 3ec38b06..e37874e9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -1,15 +1,92 @@ package lab.en2b.quizapi.auth.jwt; -import org.springframework.security.core.userdetails.UserDetails; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.function.Function; + @Component public class JwtService { - public String extractUsername(String token) { - throw new UnsupportedOperationException(); + + private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + + @Value("${JWT_SECRET}") + private String jwtSecret; + + @Value("${JWT_EXPIRATION_MS}") + private int jwtExpirationMs; + + public String generateJwtTokenUserPassword(Authentication authentication) { + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + return Jwts.builder() + .subject(userPrincipal.getEmail()) + .issuedAt(new Date()) + .claim("type","user-password") + .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(getSignInKey()) + .compact(); } - public boolean validateToken(String token, UserDetails userDetails) { + public String generateTokenFromUsername(String email) { + return Jwts.builder() + .subject(email) + .issuedAt(new Date()) + .claim("type","user-password") + .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(getSignInKey()) + .compact(); + } + public String extractUsername(String token) { throw new UnsupportedOperationException(); } + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); + return true; + } catch (SignatureException e) { + logger.error("Invalid JWT signature: {}", e.getMessage()); + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } + private Claims extractAllClaims(String token){ + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + public T extractClaim(String token, Function claimsResolver){ + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + public String getSubjectFromJwtToken(String token) { + if(validateJwtToken(token)){ + return extractClaim(token, Claims::getSubject); + }else{ + throw new IllegalArgumentException(); + } + } + private SecretKey getSignInKey(){ + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 1e371aff..c75e9dd1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,5 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); + Optional findUserByName(String username); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index c387c7aa..592afcd9 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -13,6 +13,6 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findByUsername(username).orElseThrow()); + return UserDetailsImpl.build(userRepository.findUserByName(username).orElseThrow()); } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java index a3c58cd0..9c635bd1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -1,12 +1,9 @@ package lab.en2b.quizapi.user.role; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.*; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; @Entity @Table(name = "roles") From c65dcd769f2f48006d86ce7014d799dd71ff963e Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:24:07 +0100 Subject: [PATCH 14/45] refactor: jwt auth filter --- .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 8806cbc1..d1298a63 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -11,6 +11,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -24,23 +25,31 @@ public class JwtAuthFilter extends OncePerRequestFilter { private UserService userDetailsService; @Override - protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { - String authHeader = request.getHeader("Authorization"); - String token = null; - String username = null; - if (authHeader != null && authHeader.startsWith("Bearer ")) { - token = authHeader.substring(7); - username = jwtService.getSubjectFromJwtToken(token); - } + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseJwt(request); + String username = jwtService.getSubjectFromJwtToken(token); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && jwtService.validateJwtToken(token)) { + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, + null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } filterChain.doFilter(request, response); } + private boolean isValidJwt(String token) { + return token != null && jwtService.validateJwtToken(token); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + return null; + } } From 4dc7fc7f1abc2c70022fb51eaf7f73fbe0606397 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:27:37 +0100 Subject: [PATCH 15/45] feat: added user to role --- api/src/main/java/lab/en2b/quizapi/user/role/Role.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java index 9c635bd1..f98b63b8 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/Role.java @@ -1,10 +1,16 @@ package lab.en2b.quizapi.user.role; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.user.User; import lombok.*; +import java.util.Set; + @Entity @Table(name = "roles") @NoArgsConstructor @@ -23,4 +29,8 @@ public class Role { @NotBlank @Size(max=255) private String name; + + @ManyToMany(mappedBy ="roles") + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "roles"}) + private Set users; } From 627ed44faefe864f8a9eaeb57362197edb7b3d7c Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 3 Feb 2024 21:32:58 +0100 Subject: [PATCH 16/45] feat: jwt expiration --- api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +-- api/src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index e37874e9..a45d9e14 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -20,6 +20,7 @@ public class JwtService { private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") private String jwtSecret; @@ -32,7 +33,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { return Jwts.builder() .subject(userPrincipal.getEmail()) .issuedAt(new Date()) - .claim("type","user-password") .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(getSignInKey()) .compact(); @@ -42,7 +42,6 @@ public String generateTokenFromUsername(String email) { return Jwts.builder() .subject(email) .issuedAt(new Date()) - .claim("type","user-password") .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(getSignInKey()) .compact(); diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 8b137891..4f6f5c6b 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1 +1 @@ - +JWT_EXPIRATION_MS= 86400000 From 6f036c9f445ac0ca362995fc809e7dad76ffc06c Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 4 Feb 2024 10:31:17 +0100 Subject: [PATCH 17/45] Added my name to the readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 34fd9828..f1dab6f8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This is a base repo for the [Software Architecture course](http://arquisoft.gith | Sergio Rodríguez García | UO282598 | | Jorge Joaquín Gancedo Fernández | UO282161 | | Sergio Quintana Fernández | UO288090 | +| Diego Villanueva Berros | UO283615 | This repo is a basic application composed of several components. From e510b825e99a0c8a3ba7ac512b2ca510bd612ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 4 Feb 2024 11:01:02 +0100 Subject: [PATCH 18/45] chore: name added in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f1dab6f8..6c2e3952 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This is a base repo for the [Software Architecture course](http://arquisoft.gith | Jorge Joaquín Gancedo Fernández | UO282161 | | Sergio Quintana Fernández | UO288090 | | Diego Villanueva Berros | UO283615 | +| Gonzalo Suárez Losada | UO283928 | This repo is a basic application composed of several components. From d1d48783569fa38e1b78ed26ee970abaff8cae53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 4 Feb 2024 11:08:02 +0100 Subject: [PATCH 19/45] chore: name added in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c2e3952..dda9a07c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_en2b&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_en2b) This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html). - + ## Contributors | Nombre | UO | |---------------------------------|----------| From 204096bd93fd5ee4f39d47cc7cb18274690f29da Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:33:18 +0100 Subject: [PATCH 20/45] feat: email as jwt subject --- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 2 ++ .../lab/en2b/quizapi/auth/config/UserDetailsImpl.java | 5 +---- .../java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 7 ++++--- api/src/main/java/lab/en2b/quizapi/user/User.java | 8 ++------ .../main/java/lab/en2b/quizapi/user/UserRepository.java | 2 +- api/src/main/java/lab/en2b/quizapi/user/UserService.java | 4 ++-- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 33c11b65..082b356d 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -34,9 +34,11 @@ public PasswordEncoder passwordEncoder() { public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); userPasswordProvider.setUserDetailsService(userService); userPasswordProvider.setPasswordEncoder(passwordEncoder()); + authenticationManagerBuilder.authenticationProvider(userPasswordProvider); return authenticationManagerBuilder.build(); } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 527a52e0..99a5a03c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -9,7 +9,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import java.io.Serial; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -18,8 +17,6 @@ @Getter @AllArgsConstructor public class UserDetailsImpl implements UserDetails { - @Serial - private static final long serialVersionUID = 1L; private Long id; private String username; private String email; @@ -31,7 +28,7 @@ public static UserDetailsImpl build(User user) { for(Role role : user.getRoles()){ authorities.add(new SimpleGrantedAuthority(role.getName())); } - return new UserDetailsImpl(user.getId(),user.getName() , user.getEmail(), user.getPassword(), authorities); + return new UserDetailsImpl(user.getId(),user.getUsername() , user.getEmail(), user.getPassword(), authorities); } @Override diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d1298a63..d50480ab 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -28,10 +28,11 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseJwt(request); - String username = jwtService.getSubjectFromJwtToken(token); + String email = jwtService.getSubjectFromJwtToken(token); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + // this invokes UsernamePasswordAuthenticationToken, although it uses email as subject not username UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java index 6b3bec08..90e47230 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -10,18 +9,15 @@ import jakarta.validation.constraints.Size; import lab.en2b.quizapi.user.role.Role; import lombok.*; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import java.time.Instant; -import java.time.LocalDateTime; import java.util.Set; @Entity @Table( name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = "email"), - @UniqueConstraint(columnNames = "name") + @UniqueConstraint(columnNames = "username") }) @RequiredArgsConstructor @NoArgsConstructor @@ -38,7 +34,7 @@ public class User { @NotBlank @Size(max=255) @NonNull - private String name; + private String username; @NotBlank @Size(max = 255) diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index c75e9dd1..2c0e6e04 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,5 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByName(String username); + Optional findUserByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 592afcd9..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -12,7 +12,7 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByName(username).orElseThrow()); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } } From 72eb50a654670537b75469499aece387a3373844 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:36:56 +0100 Subject: [PATCH 21/45] chore: commented authManager --- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 082b356d..3ecdb0a5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -30,6 +30,13 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + /** + * Builds the authorization manager taking into account password encoding + * @param http the http request to secure + * @return the newly created authentication manager + * @throws Exception if something goes wrong when creating the manager + */ @Bean public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = From 35c56557b1ab392cd4a0d740e39530847459404a Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:48:21 +0100 Subject: [PATCH 22/45] feat: dummy login implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 35 +++++++++++++++++-- .../quizapi/auth/dtos/JwtResponseDto.java | 22 ++++++++++++ .../lab/en2b/quizapi/user/UserService.java | 4 +++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 01067f0f..70660e79 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,17 +1,48 @@ package lab.en2b.quizapi.auth; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.JwtResponseDto; import lab.en2b.quizapi.auth.dtos.LoginDto; import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class AuthService { - public ResponseEntity login(LoginDto loginRequest) { - throw new UnsupportedOperationException(); + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + private final UserService userService; + @Transactional + public ResponseEntity login(LoginDto loginRequest){ + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtService.generateJwtTokenUserPassword(authentication); + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + String refreshToken = userService.createRefreshToken(userDetails.getId()); + return ResponseEntity.ok(new JwtResponseDto(jwt, + refreshToken, + userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + roles)); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java new file mode 100644 index 00000000..d50c5f83 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java @@ -0,0 +1,22 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class JwtResponseDto { + private String token; + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("user_id") + private Long userId; + private String username; + private String email; + private List roles; +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..b56e7ebc 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,4 +15,8 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } + + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } } From 4a93b0d86bf259ff1f4d9d4c915591ba2c3c7686 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:51:37 +0100 Subject: [PATCH 23/45] chore: cleaned code --- .../java/lab/en2b/quizapi/auth/AuthService.java | 14 ++++++-------- .../en2b/quizapi/auth/config/UserDetailsImpl.java | 7 +++++++ .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +++ .../java/lab/en2b/quizapi/user/UserService.java | 4 ---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 70660e79..fd658897 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -31,18 +31,16 @@ public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); - String jwt = jwtService.generateJwtTokenUserPassword(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - List roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); - String refreshToken = userService.createRefreshToken(userDetails.getId()); - return ResponseEntity.ok(new JwtResponseDto(jwt, - refreshToken, + + return ResponseEntity.ok(new JwtResponseDto( + jwtService.generateJwtTokenUserPassword(authentication), + jwtService.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), - roles)); + userDetails.getStringRoles()) + ); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 99a5a03c..776062d7 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Getter @AllArgsConstructor @@ -56,4 +57,10 @@ public boolean equals(Object o) { UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } + + public List getStringRoles() { + return getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a45d9e14..8381eab3 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -27,6 +27,9 @@ public class JwtService { @Value("${JWT_EXPIRATION_MS}") private int jwtExpirationMs; + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index b56e7ebc..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,8 +15,4 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } - - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } } From 12e95d30de95af94f03fb419bc555058237a639b Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:54:20 +0100 Subject: [PATCH 24/45] chore: commented code --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index fd658897..ed92d039 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -25,7 +25,12 @@ public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final UserService userService; + + /** + * Creates a session for a user. Throws an 401 unauthorized exception otherwise + * @param loginRequest the request containing the login info + * @return a response containing a fresh jwt token and a refresh token + */ @Transactional public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( From 6563fb7f1ffb6efd34c490909bea364842a99601 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:57:05 +0100 Subject: [PATCH 25/45] chore: unused imports --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index ed92d039..886b3f6f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,20 +6,15 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; -import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class AuthService { From ca736c42ce5a0776234e76858cbaa50574b9c89b Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:22:24 +0100 Subject: [PATCH 26/45] feat: register implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 8 ++++++-- .../lab/en2b/quizapi/user/RoleRepository.java | 13 ++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 6 +++++- .../lab/en2b/quizapi/user/UserService.java | 20 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 886b3f6f..5c418dc9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,6 +6,7 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -15,12 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Set; + @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - + private final UserService userService; /** * Creates a session for a user. Throws an 401 unauthorized exception otherwise * @param loginRequest the request containing the login info @@ -44,7 +47,8 @@ public ResponseEntity login(LoginDto loginRequest){ } public ResponseEntity register(RegisterDto registerRequest) { - throw new UnsupportedOperationException(); + userService.createUser(registerRequest,Set.of("user")); + return ResponseEntity.ok("User registered successfully!"); } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java new file mode 100644 index 00000000..11401b1c --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.user; + + +import lab.en2b.quizapi.user.role.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String roleName); +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 2c0e6e04..fa3eab46 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,9 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByEmail(String email); + Optional findByEmail(String email); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..abbdb9b1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,18 +1,36 @@ package lab.en2b.quizapi.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Set; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepository userRepository; + private final RoleRepository roleRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); + return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); + } + public void createUser(RegisterDto registerRequest, Set roleNames){ + if (userRepository.existsByEmail(registerRequest.getEmail()) || userRepository.existsByUsername(registerRequest.getUsername())) { + throw new IllegalArgumentException("Error: email is already in use!"); + } + + userRepository.save(User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(new BCryptPasswordEncoder().encode(registerRequest.getPassword())) + .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) + .build()); } } From c4d0b28387b24764ef9d3b0e37dc8bdacfdd916c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:24:07 +0100 Subject: [PATCH 27/45] chore: removed unused methods --- .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 8381eab3..9e528926 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -40,18 +40,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { .signWith(getSignInKey()) .compact(); } - - public String generateTokenFromUsername(String email) { - return Jwts.builder() - .subject(email) - .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(getSignInKey()) - .compact(); - } - public String extractUsername(String token) { - throw new UnsupportedOperationException(); - } public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); From 2a0a6acaf6e4e32b088714a9e8e69f56113a743c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 18:51:37 +0100 Subject: [PATCH 28/45] chore: cleaned code --- .../lab/en2b/quizapi/auth/AuthService.java | 26 +++++++++---------- .../quizapi/auth/config/UserDetailsImpl.java | 7 +++++ .../lab/en2b/quizapi/auth/jwt/JwtService.java | 3 +++ .../lab/en2b/quizapi/user/UserService.java | 4 --- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 70660e79..886b3f6f 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,43 +6,41 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; -import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final UserService userService; + + /** + * Creates a session for a user. Throws an 401 unauthorized exception otherwise + * @param loginRequest the request containing the login info + * @return a response containing a fresh jwt token and a refresh token + */ @Transactional public ResponseEntity login(LoginDto loginRequest){ Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); - String jwt = jwtService.generateJwtTokenUserPassword(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - List roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); - String refreshToken = userService.createRefreshToken(userDetails.getId()); - return ResponseEntity.ok(new JwtResponseDto(jwt, - refreshToken, + + return ResponseEntity.ok(new JwtResponseDto( + jwtService.generateJwtTokenUserPassword(authentication), + jwtService.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), - roles)); + userDetails.getStringRoles()) + ); } public ResponseEntity register(RegisterDto registerRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 99a5a03c..776062d7 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; @Getter @AllArgsConstructor @@ -56,4 +57,10 @@ public boolean equals(Object o) { UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } + + public List getStringRoles() { + return getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index a45d9e14..8381eab3 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -27,6 +27,9 @@ public class JwtService { @Value("${JWT_EXPIRATION_MS}") private int jwtExpirationMs; + public String createRefreshToken(Long id) { + throw new UnsupportedOperationException(); + } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index b56e7ebc..48b0b497 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -15,8 +15,4 @@ public class UserService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); } - - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } } From c9ce864855a0dc50f1038b08318993cc5a3f1193 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:22:24 +0100 Subject: [PATCH 29/45] feat: register implementation --- .../lab/en2b/quizapi/auth/AuthService.java | 8 ++++++-- .../lab/en2b/quizapi/user/RoleRepository.java | 13 ++++++++++++ .../lab/en2b/quizapi/user/UserRepository.java | 6 +++++- .../lab/en2b/quizapi/user/UserService.java | 20 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 886b3f6f..5c418dc9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -6,6 +6,7 @@ import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -15,12 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Set; + @Service @RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - + private final UserService userService; /** * Creates a session for a user. Throws an 401 unauthorized exception otherwise * @param loginRequest the request containing the login info @@ -44,7 +47,8 @@ public ResponseEntity login(LoginDto loginRequest){ } public ResponseEntity register(RegisterDto registerRequest) { - throw new UnsupportedOperationException(); + userService.createUser(registerRequest,Set.of("user")); + return ResponseEntity.ok("User registered successfully!"); } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java new file mode 100644 index 00000000..11401b1c --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.user; + + +import lab.en2b.quizapi.user.role.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String roleName); +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index 2c0e6e04..fa3eab46 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -7,5 +7,9 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findUserByEmail(String email); + Optional findByEmail(String email); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 48b0b497..abbdb9b1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -1,18 +1,36 @@ package lab.en2b.quizapi.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.RegisterDto; import lombok.RequiredArgsConstructor; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Set; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepository userRepository; + private final RoleRepository roleRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return UserDetailsImpl.build(userRepository.findUserByEmail(email).orElseThrow()); + return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); + } + public void createUser(RegisterDto registerRequest, Set roleNames){ + if (userRepository.existsByEmail(registerRequest.getEmail()) || userRepository.existsByUsername(registerRequest.getUsername())) { + throw new IllegalArgumentException("Error: email is already in use!"); + } + + userRepository.save(User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(new BCryptPasswordEncoder().encode(registerRequest.getPassword())) + .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) + .build()); } } From 6f569f593e524f6645a4a1f3305adeb78bf0091f Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Mon, 5 Feb 2024 19:24:07 +0100 Subject: [PATCH 30/45] chore: removed unused methods --- .../java/lab/en2b/quizapi/auth/jwt/JwtService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java index 8381eab3..9e528926 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java @@ -40,18 +40,6 @@ public String generateJwtTokenUserPassword(Authentication authentication) { .signWith(getSignInKey()) .compact(); } - - public String generateTokenFromUsername(String email) { - return Jwts.builder() - .subject(email) - .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(getSignInKey()) - .compact(); - } - public String extractUsername(String token) { - throw new UnsupportedOperationException(); - } public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); From 9f0097f31dcb820b1d3f826e33fd8abc88de170c Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 6 Feb 2024 11:59:14 +0100 Subject: [PATCH 31/45] fix!: cors filter --- .../quizapi/auth/config/SecurityConfig.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 3ecdb0a5..1838388a 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -16,6 +16,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; @Configuration @EnableWebSecurity @@ -30,7 +33,18 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + // Configure CORS settings here + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } /** * Builds the authorization manager taking into account password encoding * @param http the http request to secure @@ -57,10 +71,9 @@ public AuthenticationManager authManager(HttpSecurity http) throws Exception { */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests(authorize -> + return http.cors(Customizer.withDefaults()).authorizeHttpRequests(authorize -> authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() .anyRequest().authenticated()) - .cors(Customizer.withDefaults()) //TODO: add exception handling .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .build(); From 633222fcef21d2cd2114ab54a0790014e345d193 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 6 Feb 2024 12:54:35 +0100 Subject: [PATCH 32/45] chore: moved RoleRepository --- api/src/main/java/lab/en2b/quizapi/user/UserService.java | 1 + .../java/lab/en2b/quizapi/user/{ => role}/RoleRepository.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename api/src/main/java/lab/en2b/quizapi/user/{ => role}/RoleRepository.java (89%) diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index abbdb9b1..5991827a 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -2,6 +2,7 @@ import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.user.role.RoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java similarity index 89% rename from api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java rename to api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java index 11401b1c..cfdc0db1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/RoleRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.user.role; import lab.en2b.quizapi.user.role.Role; From 70f7215ff007e832d023ca1a7b751aac885f5cff Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 17:49:28 +0100 Subject: [PATCH 33/45] feat: database settings -> environment variables --- api/src/main/resources/application.properties | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 4f6f5c6b..c6bad700 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1 +1,5 @@ -JWT_EXPIRATION_MS= 86400000 +JWT_EXPIRATION_MS=86400000 +spring.jpa.hibernate.ddl-auto=update +spring.datasource.url=${DATABASE_URL} +spring.datasource.username=${DATABASE_USER} +spring.datasource.password=${DATABASE_PASSWORD} From ac9d6528b441bf1c9578dde8417366e4d0148a84 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 18:02:33 +0100 Subject: [PATCH 34/45] fix: added missing request body --- api/src/main/java/lab/en2b/quizapi/auth/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index dd755545..1a311a0a 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -28,7 +28,7 @@ public ResponseEntity loginUser(@Valid @RequestBody LoginDto loginRequest){ } @PostMapping("/refresh-token") - public ResponseEntity refreshToken(@Valid RefreshTokenDto refreshTokenRequest){ + public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenDto refreshTokenRequest){ return authService.refreshToken(refreshTokenRequest); } } From a3164b2d562086938eeb8b66008c36423d2035cc Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 18:44:22 +0100 Subject: [PATCH 35/45] fix: added missing dependencies --- api/pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/pom.xml b/api/pom.xml index bba945c2..20ba80be 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -30,6 +30,7 @@ org.springframework.boot spring-boot-starter-security + 3.1.4 @@ -77,6 +78,14 @@ tomcat-embed-core 10.1.13 + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + From e99a2e15aeaf7fcc7094e938e11f3699eb5f9ccc Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 23:09:11 +0100 Subject: [PATCH 36/45] refactor: JwtService -> JwtUtils --- .../main/java/lab/en2b/quizapi/auth/AuthService.java | 9 +++++---- .../lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 12 ++++++++---- .../auth/jwt/{JwtService.java => JwtUtils.java} | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) rename api/src/main/java/lab/en2b/quizapi/auth/jwt/{JwtService.java => JwtUtils.java} (98%) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 5c418dc9..84d9a744 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -5,7 +5,7 @@ import lab.en2b.quizapi.auth.dtos.LoginDto; import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; -import lab.en2b.quizapi.auth.jwt.JwtService; +import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,9 +19,10 @@ import java.util.Set; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { - private final JwtService jwtService; + private final JwtUtils jwtUtils; private final AuthenticationManager authenticationManager; private final UserService userService; /** @@ -37,8 +38,8 @@ public ResponseEntity login(LoginDto loginRequest){ UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); return ResponseEntity.ok(new JwtResponseDto( - jwtService.generateJwtTokenUserPassword(authentication), - jwtService.createRefreshToken(userDetails.getId()), + jwtUtils.generateJwtTokenUserPassword(authentication), + jwtUtils.createRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index d50480ab..0014c0b8 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -19,7 +19,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Autowired - private JwtService jwtService; + private JwtUtils jwtUtils; @Autowired private UserService userDetailsService; @@ -28,9 +28,13 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseJwt(request); - String email = jwtService.getSubjectFromJwtToken(token); + String email = null; + if(token != null){ + email = jwtUtils.getSubjectFromJwtToken(token); + } + System.out.println("entered"); - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { + if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); // this invokes UsernamePasswordAuthenticationToken, although it uses email as subject not username UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, @@ -42,7 +46,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } private boolean isValidJwt(String token) { - return token != null && jwtService.validateJwtToken(token); + return token != null && jwtUtils.validateJwtToken(token); } private String parseJwt(HttpServletRequest request) { diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java similarity index 98% rename from api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java rename to api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index 9e528926..45f49f8c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -16,9 +16,9 @@ import java.util.function.Function; @Component -public class JwtService { +public class JwtUtils { - private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") From 7820bf586be59c5796a6c30d391e380af7f5d3c9 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 6 Feb 2024 23:09:52 +0100 Subject: [PATCH 37/45] feat: updated pom dependencies --- api/pom.xml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 20ba80be..c16846b8 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -20,17 +20,22 @@ org.springframework.boot spring-boot-starter-data-jpa - 3.1.4 + 3.2.2 - jakarta.validation - jakarta.validation-api - 3.0.2 + org.springframework.boot + spring-boot-starter-web + 3.2.2 org.springframework.boot spring-boot-starter-security - 3.1.4 + 3.2.2 + + + jakarta.validation + jakarta.validation-api + 3.0.2 @@ -73,19 +78,6 @@ jjwt-jackson 0.12.1 - - org.apache.tomcat.embed - tomcat-embed-core - 10.1.13 - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-web - From 5a0c70c2297c52bce5b83e8144d3198c9d178ef8 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:05:34 +0100 Subject: [PATCH 38/45] feat: refresh token --- .../lab/en2b/quizapi/auth/AuthService.java | 15 ++--- .../quizapi/auth/config/SecurityConfig.java | 56 +++++++++---------- .../auth/dtos/RefreshTokenResponseDto.java | 20 +++++++ .../en2b/quizapi/auth/jwt/JwtAuthFilter.java | 1 - .../lab/en2b/quizapi/auth/jwt/JwtUtils.java | 22 +++++--- .../exceptions/TokenRefreshException.java | 7 +++ .../main/java/lab/en2b/quizapi/user/User.java | 8 +++ .../lab/en2b/quizapi/user/UserRepository.java | 2 + .../lab/en2b/quizapi/user/UserService.java | 18 ++++++ api/src/main/resources/application.properties | 1 + 10 files changed, 103 insertions(+), 47 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 84d9a744..c132fbe5 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -1,11 +1,11 @@ package lab.en2b.quizapi.auth; import lab.en2b.quizapi.auth.config.UserDetailsImpl; -import lab.en2b.quizapi.auth.dtos.JwtResponseDto; -import lab.en2b.quizapi.auth.dtos.LoginDto; -import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; -import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.auth.dtos.*; import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; +import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto; +import lab.en2b.quizapi.user.User; import lab.en2b.quizapi.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,7 +19,6 @@ import java.util.Set; @Service -@Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { private final JwtUtils jwtUtils; @@ -39,7 +38,7 @@ public ResponseEntity login(LoginDto loginRequest){ return ResponseEntity.ok(new JwtResponseDto( jwtUtils.generateJwtTokenUserPassword(authentication), - jwtUtils.createRefreshToken(userDetails.getId()), + userService.assignNewRefreshToken(userDetails.getId()), userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), @@ -53,6 +52,8 @@ public ResponseEntity register(RegisterDto registerRequest) { } public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { - throw new UnsupportedOperationException(); + User user = userService.findByRefreshToken(refreshTokenRequest.getRefreshToken()).orElseThrow(() -> new TokenRefreshException( + "Refresh token is not in database!")); + return ResponseEntity.ok(new RefreshTokenResponseDto(jwtUtils.generateTokenFromEmail(user.getEmail()), user.obtainRefreshIfValid())); } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 1838388a..381a1cd6 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,17 +1,17 @@ package lab.en2b.quizapi.auth.config; import lab.en2b.quizapi.user.UserService; -import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -22,11 +22,8 @@ @Configuration @EnableWebSecurity -@EnableMethodSecurity +@RequiredArgsConstructor public class SecurityConfig { - - @Autowired - private JwtAuthFilter authFilter; @Autowired public UserService userService; @Bean @@ -45,24 +42,6 @@ public CorsFilter corsFilter() { source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } - /** - * Builds the authorization manager taking into account password encoding - * @param http the http request to secure - * @return the newly created authentication manager - * @throws Exception if something goes wrong when creating the manager - */ - @Bean - public AuthenticationManager authManager(HttpSecurity http) throws Exception { - AuthenticationManagerBuilder authenticationManagerBuilder = - http.getSharedObject(AuthenticationManagerBuilder.class); - - DaoAuthenticationProvider userPasswordProvider = new DaoAuthenticationProvider(); - userPasswordProvider.setUserDetailsService(userService); - userPasswordProvider.setPasswordEncoder(passwordEncoder()); - - authenticationManagerBuilder.authenticationProvider(userPasswordProvider); - return authenticationManagerBuilder.build(); - } /** * Security filter used for filtering all petitions, applying cors and asking for authentication among other things * @param http the http request to filter @@ -70,14 +49,29 @@ public AuthenticationManager authManager(HttpSecurity http) throws Exception { * @throws Exception if any problem happens when filtering */ @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.cors(Customizer.withDefaults()).authorizeHttpRequests(authorize -> - authorize.requestMatchers("/auth/login", "/auth/register","/auth/refresh-token").permitAll() - .anyRequest().authenticated()) - //TODO: add exception handling + public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { + return http + .cors(Customizer.withDefaults()) .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers(HttpMethod.POST,"/auth/**").permitAll() + .anyRequest().authenticated()) + .csrf(AbstractHttpConfigurer::disable) + .authenticationManager(authenticationManager) .build(); + //TODO: add exception handling } - + /** + * Builds the authorization manager taking into account password encoding + * @param http the http request to secure + * @return the newly created authentication manager + * @throws Exception if something goes wrong when creating the manager + */ + @Bean + public AuthenticationManager authManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder()); + return authenticationManagerBuilder.build(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java new file mode 100644 index 00000000..4af19ee2 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java @@ -0,0 +1,20 @@ +package lab.en2b.quizapi.auth.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RefreshTokenResponseDto { + + private String token; + @JsonProperty("refresh_token") + private String refreshToken; + + public RefreshTokenResponseDto(String accessToken, String refreshToken) { + this.token = accessToken; + this.refreshToken = refreshToken; + } + +} diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 0014c0b8..f5ee7832 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -32,7 +32,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht if(token != null){ email = jwtUtils.getSubjectFromJwtToken(token); } - System.out.println("entered"); if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index 45f49f8c..bc782dfd 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -22,21 +22,18 @@ public class JwtUtils { //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") - private String jwtSecret; - + private String JWT_SECRET; @Value("${JWT_EXPIRATION_MS}") - private int jwtExpirationMs; + private Long JWT_EXPIRATION_MS; + - public String createRefreshToken(Long id) { - throw new UnsupportedOperationException(); - } public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); return Jwts.builder() .subject(userPrincipal.getEmail()) .issuedAt(new Date()) - .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS)) .signWith(getSignInKey()) .compact(); } @@ -76,7 +73,16 @@ public String getSubjectFromJwtToken(String token) { } } private SecretKey getSignInKey(){ - byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); return Keys.hmacShaKeyFor(keyBytes); } + + public String generateTokenFromEmail(String email) { + return Jwts.builder() + .subject(email) + .issuedAt(new Date()) + .expiration(new Date((new Date()).getTime() + JWT_EXPIRATION_MS)) + .signWith(getSignInKey()) + .compact(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java new file mode 100644 index 00000000..3271e6cf --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/TokenRefreshException.java @@ -0,0 +1,7 @@ +package lab.en2b.quizapi.commons.exceptions; + +public class TokenRefreshException extends RuntimeException{ + public TokenRefreshException(String message) { + super(message); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/user/User.java index 90e47230..b0abc6bc 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/user/User.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; import lab.en2b.quizapi.user.role.Role; import lombok.*; @@ -65,4 +66,11 @@ public class User { @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "permissions"}) @JsonProperty("role") private Set roles; + + public String obtainRefreshIfValid() { + if(getRefreshExpiration() == null || getRefreshExpiration().compareTo(Instant.now()) < 0){ + throw new TokenRefreshException( "Invalid refresh token. Please make a new login request"); + } + return getRefreshToken(); + } } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java index fa3eab46..36a9fbe4 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository { Boolean existsByUsername(String username); Boolean existsByEmail(String email); + + Optional findByRefreshToken(String refreshToken); } diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/user/UserService.java index 5991827a..55b82123 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/user/UserService.java @@ -4,13 +4,17 @@ import lab.en2b.quizapi.auth.dtos.RegisterDto; import lab.en2b.quizapi.user.role.RoleRepository; import lombok.RequiredArgsConstructor; +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.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import java.time.Instant; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -18,6 +22,8 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final RoleRepository roleRepository; + @Value("${REFRESH_TOKEN_DURATION_MS}") + private Long REFRESH_TOKEN_DURATION_MS; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); @@ -34,4 +40,16 @@ public void createUser(RegisterDto registerRequest, Set roleNames){ .roles(roleNames.stream().map( roleName -> roleRepository.findByName(roleName).orElseThrow()).collect(Collectors.toSet())) .build()); } + + public Optional findByRefreshToken(String refreshToken) { + return userRepository.findByRefreshToken(refreshToken); + } + + public String assignNewRefreshToken(Long id) { + User user = userRepository.findById(id).orElseThrow(); + user.setRefreshToken(UUID.randomUUID().toString()); + user.setRefreshExpiration(Instant.now().plusMillis(REFRESH_TOKEN_DURATION_MS)); + userRepository.save(user); + return user.getRefreshToken(); + } } diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index c6bad700..8419350a 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1,4 +1,5 @@ JWT_EXPIRATION_MS=86400000 +REFRESH_TOKEN_DURATION_MS=86400000 spring.jpa.hibernate.ddl-auto=update spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USER} From c9091c3ea5f902ef0e3d65cd69f40e3ca23b451f Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:13:06 +0100 Subject: [PATCH 39/45] feat: exception handling --- .../exceptions/CustomControllerAdvice.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java new file mode 100644 index 00000000..7032fbf4 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java @@ -0,0 +1,61 @@ +package lab.en2b.quizapi.commons.exceptions; + +import java.util.NoSuchElementException; + +import lombok.extern.log4j.Log4j2; +import org.springframework.core.annotation.Order; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +@Log4j2 +@Order(Ordered.HIGHEST_PRECEDENCE) +public class CustomControllerAdvice extends ResponseEntityExceptionHandler { + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNoSuchElementException(NoSuchElementException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentialsException(BadCredentialsException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(), HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(TokenRefreshException.class) + public ResponseEntity handleTokenRefreshException(TokenRefreshException exception) { + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.FORBIDDEN); + } + @ExceptionHandler(InternalAuthenticationServiceException.class) + public ResponseEntity handleInternalAuthenticationServiceException(InternalAuthenticationServiceException exception) { + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.FORBIDDEN); + } + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR); + } + +} From 6b18c5cdf5da0b72f8dd6a16f825b96ff4a1554b Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:15:14 +0100 Subject: [PATCH 40/45] refactor: moved user folder --- api/src/main/java/lab/en2b/quizapi/auth/AuthService.java | 4 ++-- .../java/lab/en2b/quizapi/auth/config/SecurityConfig.java | 2 +- .../java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java | 4 ++-- .../main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java | 2 +- .../main/java/lab/en2b/quizapi/{ => commons}/user/User.java | 4 ++-- .../lab/en2b/quizapi/{ => commons}/user/UserRepository.java | 2 +- .../lab/en2b/quizapi/{ => commons}/user/UserService.java | 4 ++-- .../java/lab/en2b/quizapi/{ => commons}/user/role/Role.java | 6 ++---- .../quizapi/{ => commons}/user/role/RoleRepository.java | 3 +-- 9 files changed, 14 insertions(+), 17 deletions(-) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/User.java (95%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/UserRepository.java (91%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/UserService.java (96%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/role/Role.java (79%) rename api/src/main/java/lab/en2b/quizapi/{ => commons}/user/role/RoleRepository.java (78%) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index c132fbe5..82e39000 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -5,8 +5,8 @@ import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto; -import lab.en2b.quizapi.user.User; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 381a1cd6..85379e4c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.auth.config; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 776062d7..7690b1d2 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -1,8 +1,8 @@ package lab.en2b.quizapi.auth.config; import com.fasterxml.jackson.annotation.JsonIgnore; -import lab.en2b.quizapi.user.User; -import lab.en2b.quizapi.user.role.Role; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.role.Role; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index f5ee7832..9f056081 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -3,7 +3,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lab.en2b.quizapi.user.UserService; +import lab.en2b.quizapi.commons.user.UserService; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/api/src/main/java/lab/en2b/quizapi/user/User.java b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java similarity index 95% rename from api/src/main/java/lab/en2b/quizapi/user/User.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/User.java index b0abc6bc..d6f041cb 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lab.en2b.quizapi.commons.exceptions.TokenRefreshException; -import lab.en2b.quizapi.user.role.Role; +import lab.en2b.quizapi.commons.user.role.Role; import lombok.*; import java.time.Instant; diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java similarity index 91% rename from api/src/main/java/lab/en2b/quizapi/user/UserRepository.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java index 36a9fbe4..780f15cf 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserRepository.java @@ -1,4 +1,4 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/api/src/main/java/lab/en2b/quizapi/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java similarity index 96% rename from api/src/main/java/lab/en2b/quizapi/user/UserService.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index 55b82123..d6ed64d9 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -1,8 +1,8 @@ -package lab.en2b.quizapi.user; +package lab.en2b.quizapi.commons.user; import lab.en2b.quizapi.auth.config.UserDetailsImpl; import lab.en2b.quizapi.auth.dtos.RegisterDto; -import lab.en2b.quizapi.user.role.RoleRepository; +import lab.en2b.quizapi.commons.user.role.RoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java b/api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java similarity index 79% rename from api/src/main/java/lab/en2b/quizapi/user/role/Role.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java index f98b63b8..8e828ff1 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/Role.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/role/Role.java @@ -1,12 +1,10 @@ -package lab.en2b.quizapi.user.role; +package lab.en2b.quizapi.commons.user.role; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lab.en2b.quizapi.user.User; +import lab.en2b.quizapi.commons.user.User; import lombok.*; import java.util.Set; diff --git a/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java b/api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java similarity index 78% rename from api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java rename to api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java index cfdc0db1..84c7a7a2 100644 --- a/api/src/main/java/lab/en2b/quizapi/user/role/RoleRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/role/RoleRepository.java @@ -1,7 +1,6 @@ -package lab.en2b.quizapi.user.role; +package lab.en2b.quizapi.commons.user.role; -import lab.en2b.quizapi.user.role.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; From 8dd1e5c4b8fe43fe2b402047a614293faee4b951 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 7 Feb 2024 00:25:11 +0100 Subject: [PATCH 41/45] feat: finished auth --- .../en2b/quizapi/auth/config/SecurityConfig.java | 7 +++++++ .../java/lab/en2b/quizapi/commons/user/User.java | 2 +- .../questions/question/QuestionController.java | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 85379e4c..321fb182 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.auth.config; +import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -27,6 +29,10 @@ public class SecurityConfig { @Autowired public UserService userService; @Bean + public JwtAuthFilter authenticationJwtTokenFilter() { + return new JwtAuthFilter(); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @@ -58,6 +64,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication .anyRequest().authenticated()) .csrf(AbstractHttpConfigurer::disable) .authenticationManager(authenticationManager) + .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class) .build(); //TODO: add exception handling } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/User.java b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java index d6f041cb..56d64935 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/User.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/User.java @@ -56,7 +56,7 @@ public class User { private Instant refreshExpiration; @NotNull - @ManyToMany + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name="users_roles", joinColumns= @JoinColumn(name="user_id", referencedColumnName="id"), diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java new file mode 100644 index 00000000..1c1e7127 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.questions.question; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/questions") +public class QuestionController { + @GetMapping("/dummy") + private String getDummyQuestion(){ + return "Who the hell is Steve Jobs?"; + } +} From c3947c766f99929bd5833054311fafcf854e0e91 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Wed, 7 Feb 2024 09:13:15 +0100 Subject: [PATCH 42/45] chore: used same logger as CustomControllerAdvice --- .../java/lab/en2b/quizapi/auth/jwt/JwtUtils.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index bc782dfd..432b0813 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -5,8 +5,7 @@ import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import lab.en2b.quizapi.auth.config.UserDetailsImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -16,10 +15,9 @@ import java.util.function.Function; @Component +@Log4j2 public class JwtUtils { - private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); - //MUST BE SET AS ENVIRONMENT VARIABLE @Value("${JWT_SECRET}") private String JWT_SECRET; @@ -42,15 +40,15 @@ public boolean validateJwtToken(String authToken) { Jwts.parser().verifyWith(getSignInKey()).build().parseSignedClaims(authToken); return true; } catch (SignatureException e) { - logger.error("Invalid JWT signature: {}", e.getMessage()); + log.error("Invalid JWT signature: {}", e.getMessage()); } catch (MalformedJwtException e) { - logger.error("Invalid JWT token: {}", e.getMessage()); + log.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { - logger.error("JWT token is expired: {}", e.getMessage()); + log.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { - logger.error("JWT token is unsupported: {}", e.getMessage()); + log.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { - logger.error("JWT claims string is empty: {}", e.getMessage()); + log.error("JWT claims string is empty: {}", e.getMessage()); } return false; } From 4e1f1da9e8d87c8d49a666213890d33724c9a437 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 14 Feb 2024 22:09:11 +0100 Subject: [PATCH 43/45] chore: useless files --- api/.gitignore | 2 +- api/.mvn/wrapper/maven-wrapper.jar | Bin 62547 -> 0 bytes api/.mvn/wrapper/maven-wrapper.properties | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 api/.mvn/wrapper/maven-wrapper.jar delete mode 100644 api/.mvn/wrapper/maven-wrapper.properties diff --git a/api/.gitignore b/api/.gitignore index 549e00a2..d2546f0d 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,6 +1,6 @@ HELP.md target/ -!.mvn/wrapper/maven-wrapper.jar +!.mvn/ !**/src/main/**/target/ !**/src/test/**/target/ diff --git a/api/.mvn/wrapper/maven-wrapper.jar b/api/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index cb28b0e37c7d206feb564310fdeec0927af4123a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62547 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 A?*IS* diff --git a/api/.mvn/wrapper/maven-wrapper.properties b/api/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 5f0536eb..00000000 --- a/api/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar From 065d6c02318fcbd1026e007db4fcd82b345d327f Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 14 Feb 2024 22:11:42 +0100 Subject: [PATCH 44/45] chore: .idea --- .idea/.gitignore | 3 --- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ .idea/wiq_en2b.iml | 9 --------- 5 files changed, 32 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/wiq_en2b.iml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 07115cdf..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5668832c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/wiq_en2b.iml b/.idea/wiq_en2b.iml deleted file mode 100644 index d6ebd480..00000000 --- a/.idea/wiq_en2b.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From f3009023745bb05b177101eb7402ee6cd9930798 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 14 Feb 2024 22:09:11 +0100 Subject: [PATCH 45/45] chore: useless files --- .idea/.gitignore | 3 --- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ .idea/wiq_en2b.iml | 9 --------- api/.gitignore | 2 +- api/.mvn/wrapper/maven-wrapper.jar | Bin 62547 -> 0 bytes api/.mvn/wrapper/maven-wrapper.properties | 2 -- 8 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/wiq_en2b.iml delete mode 100644 api/.mvn/wrapper/maven-wrapper.jar delete mode 100644 api/.mvn/wrapper/maven-wrapper.properties diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 07115cdf..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5668832c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/wiq_en2b.iml b/.idea/wiq_en2b.iml deleted file mode 100644 index d6ebd480..00000000 --- a/.idea/wiq_en2b.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 549e00a2..d2546f0d 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,6 +1,6 @@ HELP.md target/ -!.mvn/wrapper/maven-wrapper.jar +!.mvn/ !**/src/main/**/target/ !**/src/test/**/target/ diff --git a/api/.mvn/wrapper/maven-wrapper.jar b/api/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index cb28b0e37c7d206feb564310fdeec0927af4123a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62547 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 A?*IS* diff --git a/api/.mvn/wrapper/maven-wrapper.properties b/api/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 5f0536eb..00000000 --- a/api/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar