feat: review backend and frontend
- update to the latest version of Java/SpringBoot - update to the latest version NuxtJS - add account/password update - add account creation - add account password reset - add bundle to regroup questions and add default questions on user creation - add bundle creation
This commit is contained in:
		
							
								
								
									
										14
									
								
								backend/.env_template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								backend/.env_template
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| POSTGRES_USER=postgres | ||||
| POSTGRES_PASSWORD=LKG+gD96 | ||||
| DATABASE_HOST=localhost | ||||
| DATABASE_PORT=5432 | ||||
| DATABASE_NAME=pluss_db | ||||
|  | ||||
| MAIL_HOST= | ||||
| MAIL_PORT= | ||||
| MAIL_FROM= | ||||
| MAIL_USERNAME= | ||||
| MAIL_PASSWORD= | ||||
| MAIL_ACTIVATE_DEBUG=false | ||||
|  | ||||
| FRONTEND_URL=http://localhost:3000/ | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM maven:3.8.6-eclipse-temurin-17 as builder | ||||
| FROM maven:3.9.8-eclipse-temurin-21 as builder | ||||
|  | ||||
| WORKDIR /src | ||||
| COPY . /src | ||||
| @@ -8,7 +8,7 @@ WORKDIR /app | ||||
| RUN cp /src/target/bousole-pluss-backend*.jar bousole-pluss-backend.jar | ||||
| RUN java -Djarmode=layertools -jar bousole-pluss-backend.jar extract | ||||
|  | ||||
| FROM eclipse-temurin:17-jre-alpine | ||||
| FROM eclipse-temurin:21-jre-alpine | ||||
| EXPOSE 8080 | ||||
|  | ||||
| USER root | ||||
| @@ -18,4 +18,4 @@ COPY --from=builder /app/dependencies/ ./ | ||||
| COPY --from=builder /app/snapshot-dependencies/ ./ | ||||
| COPY --from=builder /app/spring-boot-loader/ ./ | ||||
| COPY --from=builder /app/application/ ./ | ||||
| ENTRYPOINT java org.springframework.boot.loader.JarLauncher | ||||
| ENTRYPOINT java org.springframework.boot.loader.launch.JarLauncher | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 	<parent> | ||||
| 		<groupId>org.springframework.boot</groupId> | ||||
| 		<artifactId>spring-boot-starter-parent</artifactId> | ||||
| 		<version>2.7.2</version> | ||||
| 		<version>3.2.7</version> | ||||
| 		<relativePath/> <!-- lookup parent from repository --> | ||||
| 	</parent> | ||||
| 	<groupId>fr.itsonus</groupId> | ||||
| @@ -14,9 +14,9 @@ | ||||
| 	<name>bousole-pluss-backend</name> | ||||
| 	<description>Backend projet of Bousole PLUSS project</description> | ||||
| 	<properties> | ||||
| 		<java.version>17</java.version> | ||||
| 		<java.version>21</java.version> | ||||
|  | ||||
| 		<jjwt.version>0.9.1</jjwt.version> | ||||
| 		<jjwt.version>0.12.6</jjwt.version> | ||||
| 	</properties> | ||||
| 	<dependencies> | ||||
| 		<dependency> | ||||
| @@ -31,10 +31,6 @@ | ||||
| 			<groupId>org.springframework.security</groupId> | ||||
| 			<artifactId>spring-security-data</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.springframework.hateoas</groupId> | ||||
| 			<artifactId>spring-hateoas</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-web</artifactId> | ||||
| @@ -52,15 +48,33 @@ | ||||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-security</artifactId> | ||||
| 		</dependency> | ||||
|  | ||||
| 		<dependency> | ||||
| 			<groupId>io.jsonwebtoken</groupId> | ||||
| 			<artifactId>jjwt</artifactId> | ||||
| 			<version>${jjwt.version}</version> | ||||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-mail</artifactId> | ||||
| 		</dependency> | ||||
|  | ||||
| 		<dependency> | ||||
| 			<groupId>com.h2database</groupId> | ||||
| 			<artifactId>h2</artifactId> | ||||
| 			<groupId>io.jsonwebtoken</groupId> | ||||
| 			<artifactId>jjwt-api</artifactId> | ||||
| 			<version>${jjwt.version}</version> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>io.jsonwebtoken</groupId> | ||||
| 			<artifactId>jjwt-impl</artifactId> | ||||
| 			<version>${jjwt.version}</version> | ||||
| 			<scope>runtime</scope> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>io.jsonwebtoken</groupId> | ||||
| 			<artifactId>jjwt-jackson</artifactId> | ||||
| 			<version>${jjwt.version}</version> | ||||
| 			<scope>runtime</scope> | ||||
| 		</dependency> | ||||
|  | ||||
| 		<dependency> | ||||
| 			<groupId>org.postgresql</groupId> | ||||
| 			<artifactId>postgresql</artifactId> | ||||
| 			<scope>runtime</scope> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
|   | ||||
| @@ -2,6 +2,11 @@ package fr.itsonus.bousoleplussbackend; | ||||
|  | ||||
| import org.springframework.boot.SpringApplication; | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.web.servlet.LocaleResolver; | ||||
| import org.springframework.web.servlet.i18n.SessionLocaleResolver; | ||||
|  | ||||
| import java.util.Locale; | ||||
|  | ||||
| @SpringBootApplication | ||||
| public class Application { | ||||
| @@ -9,4 +14,11 @@ public class Application { | ||||
|     public static void main(String[] args) { | ||||
|         SpringApplication.run(Application.class, args); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public LocaleResolver localeResolver() { | ||||
|         var slr = new SessionLocaleResolver(); | ||||
|         slr.setDefaultLocale(Locale.FRANCE); | ||||
|         return slr; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,72 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.configuration; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.AuthEntryPointJwt; | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.AuthTokenFilter; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsServiceImpl; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; | ||||
| import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; | ||||
| 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.configuration.WebSecurityConfigurerAdapter; | ||||
| 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.authentication.UsernamePasswordAuthenticationFilter; | ||||
| import org.springframework.web.cors.CorsConfiguration; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @Configuration | ||||
| @EnableWebSecurity | ||||
| @EnableGlobalMethodSecurity(prePostEnabled = true) | ||||
| @AllArgsConstructor | ||||
| public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | ||||
|     private final UserDetailsServiceImpl userDetailsService; | ||||
|  | ||||
|     private final AuthEntryPointJwt unauthorizedHandler; | ||||
|  | ||||
|     @Bean | ||||
|     public AuthTokenFilter authenticationJwtTokenFilter() { | ||||
|         return new AuthTokenFilter(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { | ||||
|         authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     @Override | ||||
|     public AuthenticationManager authenticationManagerBean() throws Exception { | ||||
|         return super.authenticationManagerBean(); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public PasswordEncoder passwordEncoder() { | ||||
|         return new BCryptPasswordEncoder(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void configure(HttpSecurity http) throws Exception { | ||||
|         http | ||||
|                 .cors().configurationSource(request -> { | ||||
|                     var configuration = new CorsConfiguration(); | ||||
|                     configuration.setAllowedOrigins(List.of("*")); | ||||
|                     configuration.setAllowedMethods(List.of("GET", "POST")); | ||||
|                     configuration.setAllowedHeaders(List.of("*")); | ||||
|                     return configuration; | ||||
|                 }) | ||||
|                 .and() | ||||
|                 .csrf().disable() | ||||
|                 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() | ||||
|                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() | ||||
|                 .authorizeRequests().antMatchers("/auth/**").permitAll() | ||||
|                 .anyRequest().authenticated(); | ||||
|  | ||||
|         http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,45 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.NotifyPasswordResetRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.ResetPasswordRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.UpdateAccountRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.UpdatePasswordRequest; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.UserPasswordResetUseCase; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.UserUpdateUseCase; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||
| import org.springframework.web.bind.annotation.PutMapping; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/account") | ||||
| @AllArgsConstructor | ||||
| public class AccountController { | ||||
|  | ||||
|     private final UserUpdateUseCase userUpdateUseCase; | ||||
|     private final UserPasswordResetUseCase userPasswordResetUseCase; | ||||
|  | ||||
|     @PutMapping | ||||
|     public void update(@Valid @RequestBody UpdateAccountRequest request) { | ||||
|         userUpdateUseCase.update(request.username(), request.email()); | ||||
|     } | ||||
|  | ||||
|     @PutMapping("password") | ||||
|     public void update(@Valid @RequestBody UpdatePasswordRequest request) { | ||||
|         userUpdateUseCase.updatePassword(request.currentPassword(), request.newPassword(), request.confirmationPassword()); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("password/notify-reset-request") | ||||
|     public void notifyForPasswordReset(@Valid @RequestBody NotifyPasswordResetRequest request) { | ||||
|         userPasswordResetUseCase.notifyForPasswordReset(request.email()); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("password/reset") | ||||
|     public void resetPassword(@Valid @RequestBody ResetPasswordRequest request) { | ||||
|         userPasswordResetUseCase.resetPassword(request.token(), request.email(), request.newPassword(), request.confirmationPassword()); | ||||
|     } | ||||
| } | ||||
| @@ -1,101 +1,85 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.AuthenticationService; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.RefreshToken; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.exception.TokenRefreshException; | ||||
| import fr.itsonus.bousoleplussbackend.models.RefreshToken; | ||||
| import fr.itsonus.bousoleplussbackend.models.User; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.LogOutRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.LoginRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.SignupRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.RegisterRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.TokenRefreshRequest; | ||||
| import fr.itsonus.bousoleplussbackend.payload.response.JwtResponse; | ||||
| import fr.itsonus.bousoleplussbackend.payload.response.MessageResponse; | ||||
| import fr.itsonus.bousoleplussbackend.payload.response.TokenRefreshResponse; | ||||
| import fr.itsonus.bousoleplussbackend.repositories.UserRepository; | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.JwtUtils; | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.JwtGenerator; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.RefreshTokenService; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.UserCreationUseCase; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.validation.Valid; | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/auth") | ||||
| @AllArgsConstructor | ||||
| public class AuthController { | ||||
|  | ||||
|     private final AuthenticationManager authenticationManager; | ||||
|  | ||||
|     private final UserRepository userRepository; | ||||
|  | ||||
|     private final PasswordEncoder encoder; | ||||
|  | ||||
|     private final JwtUtils jwtUtils; | ||||
|  | ||||
|     private final DaoAuthenticationProvider userAuthenticationProvider; | ||||
|     private final AuthenticationService authenticationService; | ||||
|     private final UserCreationUseCase userCreationUseCase; | ||||
|     private final JwtGenerator jwtGenerator; | ||||
|     private final RefreshTokenService refreshTokenService; | ||||
|  | ||||
|     @PostMapping("/signin") | ||||
|     public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { | ||||
|     @PostMapping("/login") | ||||
|     public JwtResponse login(@Valid @RequestBody LoginRequest loginRequest) { | ||||
|  | ||||
|         var authentication = authenticationManager | ||||
|                 .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); | ||||
|         var authentication = userAuthenticationProvider | ||||
|                 .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); | ||||
|         SecurityContextHolder.getContext().setAuthentication(authentication); | ||||
|  | ||||
|         var userDetails = (UserDetailsImpl) authentication.getPrincipal(); | ||||
|         var jwt = jwtUtils.generateJwtToken(userDetails); | ||||
|         var refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); | ||||
|         var token = jwtGenerator.generateAuthToken(authentication); | ||||
|         var refreshToken = refreshTokenService.createOrUpdateRefreshToken(userDetails.getId()); | ||||
|  | ||||
|         return ResponseEntity.ok(new JwtResponse(jwt, refreshToken.getToken(), userDetails.getId(), | ||||
|                 userDetails.getUsername())); | ||||
|         return new JwtResponse(token, refreshToken.getToken()); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/signup") | ||||
|     public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) { | ||||
|         if (userRepository.existsByUsername(signUpRequest.getUsername())) { | ||||
|             return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!")); | ||||
|         } | ||||
|  | ||||
|         // Create new user's account | ||||
|         var user = new User(); | ||||
|         user.setUsername(signUpRequest.getUsername()); | ||||
|         user.setPassword(encoder.encode(signUpRequest.getPassword())); | ||||
|         userRepository.save(user); | ||||
|  | ||||
|         return ResponseEntity.ok(new MessageResponse("User registered successfully!")); | ||||
|     @PostMapping("/register") | ||||
|     @ResponseStatus(HttpStatus.CREATED) | ||||
|     public void registerUser(@Valid @RequestBody RegisterRequest request) { | ||||
|         userCreationUseCase.createUserAndDefaultBundle(request.toUser()); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/refreshtoken") | ||||
|     @PostMapping("/refresh-token") | ||||
|     public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) { | ||||
|         String requestRefreshToken = request.getRefreshToken(); | ||||
|  | ||||
|         var requestRefreshToken = request.getRefreshToken(); | ||||
|         return refreshTokenService.findByToken(requestRefreshToken) | ||||
|                 .map(refreshTokenService::verifyExpiration) | ||||
|                 .map(RefreshToken::getUser) | ||||
|                 .map(user -> { | ||||
|                     String token = jwtUtils.generateTokenFromUsername(user.getUsername()); | ||||
|                     return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken)); | ||||
|                     var token = jwtGenerator.generateRefreshToken(user.getEmail()); | ||||
|                     return ResponseEntity.ok(token); | ||||
|                 }) | ||||
|                 .orElseThrow(() -> new TokenRefreshException(requestRefreshToken, | ||||
|                         "Refresh token is not in database!")); | ||||
|                 .orElseThrow(() -> new TokenRefreshException(requestRefreshToken, "Refresh token unknown")); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/logout") | ||||
|     public ResponseEntity<?> logoutUser(@Valid @RequestBody LogOutRequest logOutRequest) { | ||||
|     public void logoutUser(@Valid @RequestBody LogOutRequest logOutRequest) { | ||||
|         refreshTokenService.deleteByUserId(logOutRequest.getUserId()); | ||||
|         return ResponseEntity.ok(new MessageResponse("Log out successful!")); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/me") | ||||
|     public ResponseEntity<?> me() { | ||||
|         var authentication = SecurityContextHolder.getContext().getAuthentication(); | ||||
|         return ResponseEntity.ok(authentication.getPrincipal()); | ||||
|     public User me() { | ||||
|         return authenticationService.getCurrentUser(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,22 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Axe; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.AxeUseCase; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/axes") | ||||
| @AllArgsConstructor | ||||
| public class AxeController { | ||||
|  | ||||
|     private final AxeUseCase axeRepository; | ||||
|  | ||||
|     @GetMapping | ||||
|     public Iterable<Axe> findAll() { | ||||
|         return axeRepository.findAll(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Bundle; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.BundleCreationRequest; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.BundleUseCase; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RequestParam; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @Slf4j | ||||
| @RestController | ||||
| @RequestMapping("/bundles") | ||||
| @AllArgsConstructor | ||||
| public class BundleController { | ||||
|  | ||||
|     private final BundleUseCase bundleUseCase; | ||||
|  | ||||
|     @PostMapping | ||||
|     public Bundle create(@Valid @RequestBody BundleCreationRequest request) { | ||||
|         return bundleUseCase.create(request.label(), request.presentation(), request.questions().stream().map(BundleCreationRequest.QuestionCreationRequest::toQuestion).toList()); | ||||
|     } | ||||
|  | ||||
|     @GetMapping | ||||
|     public List<Bundle> findAllForCurrentUser() { | ||||
|         return bundleUseCase.findAllForCurrentUser(); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("{id}") | ||||
|     public Bundle findById(@PathVariable("id") Long id) { | ||||
|         return bundleUseCase.findById(id); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("{id}/questions/search") | ||||
|     public Iterable<Question> findQuestions(@PathVariable Long id, @RequestParam Long axeId) { | ||||
|         return bundleUseCase.findQuestions(id, axeId); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.QuestionUseCase; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RequestParam; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @Slf4j | ||||
| @RestController | ||||
| @RequestMapping("/questions") | ||||
| @AllArgsConstructor | ||||
| public class QuestionController { | ||||
|  | ||||
|     private final QuestionUseCase questionUseCase; | ||||
|  | ||||
|     @GetMapping("search/defaults") | ||||
|     public List<Question> findDefaultsByAxeId(@RequestParam Long axeId) { | ||||
|         return questionUseCase.findDefaults(axeId); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("search") | ||||
|     public List<Question> findAllByAxeId(@RequestParam Long axeId) { | ||||
|         return questionUseCase.findAllByAxeId(axeId); | ||||
|     } | ||||
| } | ||||
| @@ -1,58 +1,42 @@ | ||||
| package fr.itsonus.bousoleplussbackend.controllers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.models.QuizScore; | ||||
| import fr.itsonus.bousoleplussbackend.models.Response; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.QuizDetailed; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.QuizRequest; | ||||
| import fr.itsonus.bousoleplussbackend.repositories.QuestionRepository; | ||||
| import fr.itsonus.bousoleplussbackend.repositories.QuizRepository; | ||||
| import fr.itsonus.bousoleplussbackend.repositories.QuizScoreRepository; | ||||
| import fr.itsonus.bousoleplussbackend.repositories.ResponseRepository; | ||||
| import fr.itsonus.bousoleplussbackend.usecase.QuizUseCase; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.data.domain.Page; | ||||
| import org.springframework.data.domain.Pageable; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RequestParam; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.validation.Valid; | ||||
| import java.util.NoSuchElementException; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @Slf4j | ||||
| @RestController | ||||
| @RequestMapping("/quizzes") | ||||
| @AllArgsConstructor | ||||
| public class QuizController { | ||||
|  | ||||
|     private QuizRepository quizRepository; | ||||
|     private ResponseRepository responseRepository; | ||||
|     private QuestionRepository questionRepository; | ||||
|     private QuizScoreRepository quizScoreRepository; | ||||
|     private final QuizUseCase quizUseCase; | ||||
|  | ||||
|     @PostMapping("batch") // TODO add to REST representation | ||||
|     @PostMapping | ||||
|     public Quiz create(@Valid @RequestBody QuizRequest request) { | ||||
|         var quiz = quizRepository.save(new Quiz()); | ||||
|         var responsesCreated = request.getResponses().stream().map(response -> { | ||||
|             // TODO add correct exception with correct status code | ||||
|             var question = questionRepository.findById(response.getQuestionId()) | ||||
|                     .orElseThrow(() -> new NoSuchElementException("No such question with id " + response.getQuestionId())); | ||||
|             log.info("Saving {}", response); | ||||
|             return responseRepository.save( | ||||
|                     new Response() | ||||
|                             .setScore(response.getScore()) | ||||
|                             .setComment(response.getComment()) | ||||
|                             .setQuiz(quiz) | ||||
|                             .setQuestion(question)); | ||||
|         }).collect(Collectors.toSet()); | ||||
|         quiz.setResponses(responsesCreated); | ||||
|         return quiz; | ||||
|         return quizUseCase.create(request); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("{id}/scores") | ||||
|     public Iterable<QuizScore> findScores(@PathVariable("id") Long id) { | ||||
|         return this.quizScoreRepository.findAllByQuizId(id); | ||||
|     @GetMapping("search") | ||||
|     public Page<QuizDetailed> findAllForCurrentUser(@RequestParam Long bundleId, Pageable pageable) { | ||||
|         return this.quizUseCase.findAllForCurrentUser(bundleId, pageable); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("{id}") | ||||
|     public QuizDetailed findById(@PathVariable("id") Long id) { | ||||
|         return this.quizUseCase.findById(id); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.spi.UserCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.exception.NoCurrentUserException; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class AuthenticationService { | ||||
|  | ||||
|     private final UserCacheRepository userCacheRepository; | ||||
|  | ||||
|     public User getCurrentUser() { | ||||
|         var authentication = SecurityContextHolder.getContext().getAuthentication(); | ||||
|         var userDetails = (UserDetails) authentication.getPrincipal(); | ||||
|         return userCacheRepository.findByEmail(userDetails.getUsername()) | ||||
|                 .orElseThrow(NoCurrentUserException::new); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth.exception; | ||||
|  | ||||
| public class AlreadyExistingUserException extends RuntimeException { | ||||
|     public AlreadyExistingUserException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth.model; | ||||
|  | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Builder; | ||||
| import lombok.Getter; | ||||
|  | ||||
| import java.time.Instant; | ||||
|  | ||||
| @Builder(toBuilder = true) | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public class RefreshToken { | ||||
|  | ||||
|     private Long id; | ||||
|     private User user; | ||||
|     private String token; | ||||
|     private Instant expiryDate; | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth.model; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Builder; | ||||
| import lombok.Getter; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| @Builder(toBuilder = true) | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public class User { | ||||
|  | ||||
|     private Long id; | ||||
|     private String email; | ||||
|     private String username; | ||||
|     @JsonIgnore | ||||
|     private String password; | ||||
|     @JsonIgnore | ||||
|     private LocalDateTime createdAt; | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.RefreshToken; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface RefreshTokenCacheRepository { | ||||
|     Optional<RefreshToken> findByToken(String token); | ||||
|  | ||||
|     Optional<RefreshToken> findByUser(User user); | ||||
|  | ||||
|     void deleteByUserId(long userId); | ||||
|  | ||||
|     RefreshToken save(RefreshToken refreshToken); | ||||
|  | ||||
|     void delete(RefreshToken refreshToken); | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.auth.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface UserCacheRepository { | ||||
|     Optional<User> findByEmail(String email); | ||||
|  | ||||
|     User save(User user); | ||||
|  | ||||
|     Boolean existsByEmail(String email); | ||||
|  | ||||
|     Optional<User> findById(Long id); | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.notification; | ||||
|  | ||||
| public interface NotificationService { | ||||
|  | ||||
|     void notify(String from, String to, String subject, String text); | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
|  | ||||
| @Data | ||||
| @Builder | ||||
| public class Axe { | ||||
|     private Long id; | ||||
|     private Integer identifier; | ||||
|     private String shortTitle; | ||||
|     private String title; | ||||
|     private String description; | ||||
|     private String color; | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
|  | ||||
| @Data | ||||
| @Builder(toBuilder = true) | ||||
| public class AxeResponses { | ||||
|  | ||||
|     private Integer axeIdentifier; | ||||
|     private Double average; | ||||
|     private List<QuizResponse> responses; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,18 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| @Data | ||||
| @Builder(toBuilder = true) | ||||
| public class Bundle { | ||||
|     private Long id; | ||||
|     private String label; | ||||
|     private String presentation; | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     private Date lastQuizzDate; | ||||
|     private Integer numberOfQuizzes; | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
|  | ||||
|  | ||||
| @Data | ||||
| @Builder(toBuilder = true) | ||||
| public class Question { | ||||
|  | ||||
|     private Long id; | ||||
|     private String label; | ||||
|     private String description; | ||||
|     @JsonIgnore | ||||
|     private Long axeId; | ||||
|     @JsonIgnore | ||||
|     private Integer index; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,12 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| @Data | ||||
| public class Quiz { | ||||
|  | ||||
|     private Long id; | ||||
|     private Date createdDate; | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Date; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Data | ||||
| public class QuizDetailed { | ||||
|     private Long id; | ||||
|     private Date createdDate; | ||||
|     private List<AxeResponses> axes = new ArrayList<>(); | ||||
|  | ||||
|     public static QuizDetailed from(Quiz quiz, List<QuizResponse> responses) { | ||||
|         var resume = new QuizDetailed(); | ||||
|         resume.setId(quiz.getId()); | ||||
|         resume.setCreatedDate(quiz.getCreatedDate()); | ||||
|         var sortedResponses = new HashMap<Integer, AxeResponses>(); | ||||
|         responses.forEach(quizResponse -> sortedResponses.compute(quizResponse.axeIdentifier(), (k, v) -> { | ||||
|             var axeResponses = Objects.requireNonNullElseGet(v, () -> AxeResponses.builder().axeIdentifier(k).responses(new ArrayList<>()).build()); | ||||
|             axeResponses.getResponses().add(quizResponse); | ||||
|             return axeResponses; | ||||
|         })); | ||||
|         resume.axes = sortedResponses.values().stream().toList(); | ||||
|         resume.axes.forEach(axeResponses -> { | ||||
|             var average = axeResponses.getResponses().stream().mapToDouble(QuizResponse::score).average().orElse(0.0); | ||||
|             axeResponses.setAverage(average); | ||||
|         }); | ||||
|         return resume; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.model; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
|  | ||||
| public record QuizResponse(@JsonIgnore Integer axeIdentifier, String question, Short score, String comment) { | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Axe; | ||||
|  | ||||
| public interface AxeCacheRepository { | ||||
|     Iterable<Axe> findAll(); | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Bundle; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public interface BundleCacheRepository { | ||||
|  | ||||
|     Bundle findByIdAndUserId(Long id, Long userId); | ||||
|  | ||||
|     List<Bundle> findAllByUser(User user); | ||||
|  | ||||
|     Bundle save(String label, String description, List<Question> questions, User user); | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public interface QuestionCacheRepository { | ||||
|     List<Question> findQuestions(Long axeId, Long bundleId); | ||||
|  | ||||
|     List<Question> findDefaultQuestions(); | ||||
|  | ||||
|     List<Question> findDefaultQuestions(Long axeId); | ||||
|  | ||||
|     List<Question> findAllByAxeId(Long axeId); | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import org.springframework.data.domain.Page; | ||||
| import org.springframework.data.domain.Pageable; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface QuizCacheRepository { | ||||
|  | ||||
|     Optional<Quiz> findById(Long id); | ||||
|  | ||||
|     Optional<Quiz> findByIdAndUserId(Long id, Long userId); | ||||
|  | ||||
|     Page<Quiz> findAllByBundleId(Long bundleId, Pageable pageable); | ||||
|  | ||||
|     Quiz createNew(Long bundleId, User user); | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package fr.itsonus.bousoleplussbackend.domain.quiz.spi; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.QuizResponse; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.ResponseRequest; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
|  | ||||
| public interface ResponseCacheRepository { | ||||
|  | ||||
|     List<QuizResponse> findAllByQuizId(Long quizId); | ||||
|  | ||||
|     void saveQuizzResponses(Long bundleId, Quiz quiz, Set<ResponseRequest> responses); | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package fr.itsonus.bousoleplussbackend.exception; | ||||
|  | ||||
| public class InvalidPasswordException extends RuntimeException { | ||||
|     public InvalidPasswordException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package fr.itsonus.bousoleplussbackend.exception; | ||||
|  | ||||
| public class NoCurrentUserException extends RuntimeException { | ||||
|     public NoCurrentUserException() { | ||||
|         super("No current user logged"); | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,7 @@ | ||||
| package fr.itsonus.bousoleplussbackend.exception; | ||||
|  | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||||
|  | ||||
| @ResponseStatus(HttpStatus.FORBIDDEN) | ||||
| public class TokenRefreshException extends RuntimeException { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     public TokenRefreshException(String token, String message) { | ||||
|         super(String.format("Failed for [%s]: %s", token, message)); | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| package fr.itsonus.bousoleplussbackend.exception; | ||||
|  | ||||
| public class UnknownEmailException extends RuntimeException { | ||||
|  | ||||
|     public UnknownEmailException() { | ||||
|         super("Cet e-mail est inconnu."); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,118 @@ | ||||
| package fr.itsonus.bousoleplussbackend.exception.handlers; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.exception.AlreadyExistingUserException; | ||||
| import fr.itsonus.bousoleplussbackend.exception.InvalidPasswordException; | ||||
| import fr.itsonus.bousoleplussbackend.exception.NoCurrentUserException; | ||||
| import fr.itsonus.bousoleplussbackend.exception.TokenRefreshException; | ||||
| import fr.itsonus.bousoleplussbackend.payload.response.ApiError; | ||||
| import fr.itsonus.bousoleplussbackend.exception.UnknownEmailException; | ||||
| import jakarta.validation.ConstraintViolationException; | ||||
| import org.springframework.dao.DataIntegrityViolationException; | ||||
| import org.springframework.data.rest.webmvc.ResourceNotFoundException; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.http.HttpStatusCode; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.security.access.AccessDeniedException; | ||||
| import org.springframework.security.authentication.BadCredentialsException; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||
| import org.springframework.web.bind.annotation.ResponseBody; | ||||
| import org.springframework.web.client.RestClientResponseException; | ||||
| import org.springframework.web.context.request.WebRequest; | ||||
| import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| @ControllerAdvice | ||||
| public class ExceptionsHandler extends ResponseEntityExceptionHandler { | ||||
|  | ||||
|     @ExceptionHandler({ConstraintViolationException.class}) | ||||
|     public ResponseEntity<ApiError> handleValidationExceptions(ConstraintViolationException ex) { | ||||
|         var errors = new ArrayList<ApiError.FieldError>(); | ||||
|         ex.getConstraintViolations().forEach(error -> errors.add(new ApiError.FieldError(error.getMessage(), error.getPropertyPath().toString()))); | ||||
|         return new ApiError("Saisie invalide", errors).toResponse(HttpStatus.BAD_REQUEST); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({AlreadyExistingUserException.class}) | ||||
|     public ResponseEntity<ApiError> handleConflictException(RuntimeException ex) { | ||||
|         return new ApiError(ex.getMessage()).toResponse(HttpStatus.CONFLICT); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({DataIntegrityViolationException.class}) | ||||
|     public ResponseEntity<ApiError> handleIntegrityViolationExceptions(DataIntegrityViolationException ex) { | ||||
|         return new ApiError("Saisie invalide: " + ex.getMessage()).toResponse(HttpStatus.BAD_REQUEST); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({InvalidPasswordException.class, UnknownEmailException.class}) | ||||
|     public ResponseEntity<ApiError> handleBadRequestException(RuntimeException ex) { | ||||
|         return new ApiError(ex.getMessage()).toResponse(HttpStatus.BAD_REQUEST); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({ResourceNotFoundException.class}) | ||||
|     public ResponseEntity<ApiError> handleResourceNotFoundException(ResourceNotFoundException ex) { | ||||
|         return new ApiError(ex.getMessage()).toResponse(HttpStatus.NOT_FOUND); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({AuthenticationException.class}) | ||||
|     public ResponseEntity<ApiError> handleAuthenticationException(Exception ex) { | ||||
|         return new ApiError("Authentication failed: " + ex.getMessage()).toResponse(HttpStatus.UNAUTHORIZED); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({AccessDeniedException.class, TokenRefreshException.class}) | ||||
|     public ResponseEntity<ApiError> handleAccessDeniedException(Exception ex) { | ||||
|         return new ApiError("Authentication needed: " + ex.getMessage()).toResponse(HttpStatus.FORBIDDEN); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({BadCredentialsException.class}) | ||||
|     public ResponseEntity<ApiError> handleBadCredentialsException(Exception ex) { | ||||
|         logger.error("Exception raised", ex); | ||||
|         return new ApiError("Vos identifiants sont incorrects.").toResponse(HttpStatus.UNAUTHORIZED); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({NoCurrentUserException.class}) | ||||
|     public ResponseEntity<ApiError> handleInternalErrorException(RuntimeException ex) { | ||||
|         logger.error("Internal error", ex); | ||||
|         return new ApiError(ex.getMessage()).toResponse(HttpStatus.INTERNAL_SERVER_ERROR); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({RestClientResponseException.class}) | ||||
|     @ResponseBody | ||||
|     public ResponseEntity<ApiError> handleHttpClientErrorException(RestClientResponseException ex) { | ||||
|         return new ApiError(ex.getStatusText()).toResponse(ex.getStatusCode()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, | ||||
|                                                                   HttpHeaders headers, | ||||
|                                                                   HttpStatusCode status, WebRequest request) { | ||||
|         var errors = new ArrayList<ApiError.FieldError>(); | ||||
|         ex.getBindingResult().getFieldErrors() | ||||
|                 .forEach(error -> errors.add(new ApiError.FieldError(error.getDefaultMessage(), error.getField()))); | ||||
|         ex.getBindingResult().getGlobalErrors() | ||||
|                 .forEach(error -> errors.add(new ApiError.FieldError(error.getDefaultMessage(), error.getObjectName()))); | ||||
|  | ||||
|         var apiError = new ApiError("Saisie invalide", errors); | ||||
|         return ResponseEntity.status(status).body(apiError); | ||||
|     } | ||||
|  | ||||
|     @ExceptionHandler({Throwable.class}) | ||||
|     @ResponseBody | ||||
|     public ResponseEntity<ApiError> handleGlobally(Throwable ex) { | ||||
|         if (logger.isDebugEnabled()) { | ||||
|             logger.warn(ex.getMessage(), ex); | ||||
|         } else { | ||||
|             logger.warn(ex.getMessage()); | ||||
|         } | ||||
|         return new ApiError(ex.getMessage()).toResponse(HttpStatus.INTERNAL_SERVER_ERROR); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, | ||||
|                                                              HttpStatusCode statusCode, WebRequest request) { | ||||
|         logger.debug("An exception occurred", ex); | ||||
|         return super.handleExceptionInternal(ex, new ApiError(ex.getMessage()), headers, statusCode, request); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.notification; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.notification.NotificationService; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.mail.SimpleMailMessage; | ||||
| import org.springframework.mail.javamail.JavaMailSender; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class SpringMailService implements NotificationService { | ||||
|  | ||||
|     private final JavaMailSender mailSender; | ||||
|  | ||||
|     @Override | ||||
|     public void notify(String from, String to, String subject, String text) { | ||||
|         var message = new SimpleMailMessage(); | ||||
|         message.setFrom(from); | ||||
|         message.setTo(to); | ||||
|         message.setSubject(subject); | ||||
|         message.setText(text); | ||||
|         mailSender.send(message); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresRefreshTokenDto; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresUserDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface RefreshTokenCachePostgresRepository extends Repository<PostgresRefreshTokenDto, Long> { | ||||
|  | ||||
|     Optional<PostgresRefreshTokenDto> findByToken(String token); | ||||
|  | ||||
|     Optional<PostgresRefreshTokenDto> findByUser(PostgresUserDto user); | ||||
|  | ||||
|     void deleteByUserId(long userId); | ||||
|  | ||||
|     PostgresRefreshTokenDto save(PostgresRefreshTokenDto refreshToken); | ||||
|  | ||||
|     void delete(PostgresRefreshTokenDto refreshToken); | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.RefreshToken; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.spi.RefreshTokenCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresRefreshTokenDto; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresUserDto; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class RefreshTokenCacheProxyRepository implements RefreshTokenCacheRepository { | ||||
|  | ||||
|     private RefreshTokenCachePostgresRepository refreshTokenRepository; | ||||
|  | ||||
|     @Override | ||||
|     public Optional<RefreshToken> findByToken(String token) { | ||||
|         return refreshTokenRepository.findByToken(token).map(PostgresRefreshTokenDto::toRefreshToken); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Optional<RefreshToken> findByUser(User user) { | ||||
|         return refreshTokenRepository.findByUser(new PostgresUserDto(user)).map(PostgresRefreshTokenDto::toRefreshToken); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteByUserId(long userId) { | ||||
|         refreshTokenRepository.deleteByUserId(userId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public RefreshToken save(RefreshToken refreshToken) { | ||||
|         User user = refreshToken.getUser(); | ||||
|         var userDto = new PostgresUserDto(user); | ||||
|         var refreshTokenDto = new PostgresRefreshTokenDto(refreshToken); | ||||
|         refreshTokenDto.setUser(userDto); | ||||
|         return refreshTokenRepository.save(refreshTokenDto).toRefreshToken(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void delete(RefreshToken refreshToken) { | ||||
|         refreshTokenRepository.delete(new PostgresRefreshTokenDto(refreshToken)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresUserDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface UserCachePostgresRepository extends Repository<PostgresUserDto, Long> { | ||||
|     Optional<PostgresUserDto> findByEmail(String email); | ||||
|  | ||||
|     PostgresUserDto save(PostgresUserDto entity); | ||||
|  | ||||
|     Boolean existsByEmail(String email); | ||||
|  | ||||
|     Optional<PostgresUserDto> findById(Long id); | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.spi.UserCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresUserDto; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class UserCacheProxyRepository implements UserCacheRepository { | ||||
|  | ||||
|     private final UserCachePostgresRepository userRepository; | ||||
|  | ||||
|     @Override | ||||
|     public Optional<User> findByEmail(String email) { | ||||
|         return userRepository.findByEmail(email) | ||||
|                 .map(PostgresUserDto::toUser); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public User save(User user) { | ||||
|         var postgresUserDto = new PostgresUserDto(user); | ||||
|         return userRepository.save(postgresUserDto).toUser(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Boolean existsByEmail(String email) { | ||||
|         return userRepository.existsByEmail(email); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Optional<User> findById(Long id) { | ||||
|         return userRepository.findById(id) | ||||
|                 .map(PostgresUserDto::toUser); | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +1,19 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Axe; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.OneToMany; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
| 
 | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.OneToMany; | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import javax.validation.constraints.Size; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| @@ -20,8 +21,8 @@ import java.util.Set; | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| public class Axe { | ||||
| @Entity(name = "axe") | ||||
| public class PostgresAxeDto { | ||||
| 
 | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
| @@ -46,13 +47,24 @@ public class Axe { | ||||
|     @OneToMany(mappedBy = "axe") | ||||
|     @ToString.Exclude | ||||
|     @JsonIgnore | ||||
|     private Set<Question> questions; | ||||
|     private Set<PostgresQuestionDto> questions; | ||||
| 
 | ||||
|     public Axe toAxe() { | ||||
|         return Axe.builder() | ||||
|                 .id(id) | ||||
|                 .identifier(identifier) | ||||
|                 .shortTitle(shortTitle) | ||||
|                 .title(title) | ||||
|                 .description(description) | ||||
|                 .color(color) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         Axe axe = (Axe) o; | ||||
|         PostgresAxeDto axe = (PostgresAxeDto) o; | ||||
|         return id != null && Objects.equals(id, axe.id); | ||||
|     } | ||||
| 
 | ||||
| @@ -0,0 +1,71 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Bundle; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.JoinColumn; | ||||
| import jakarta.persistence.ManyToOne; | ||||
| import jakarta.persistence.OneToMany; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import lombok.Getter; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
|  | ||||
| import java.util.Comparator; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @Entity(name = "bundle") | ||||
| public class PostgresBundleDto { | ||||
|  | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 50) | ||||
|     private String label; | ||||
|  | ||||
|     private String presentation; | ||||
|  | ||||
|     @OneToMany(mappedBy = "bundle") | ||||
|     @ToString.Exclude | ||||
|     private Set<PostgresQuestionDto> questions; | ||||
|  | ||||
|     @OneToMany(mappedBy = "bundle") | ||||
|     @ToString.Exclude | ||||
|     private Set<PostgresQuizDto> quizzes; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "user_id") | ||||
|     private PostgresUserDto user; | ||||
|  | ||||
|     public Bundle toBundle() { | ||||
|         return Bundle.builder() | ||||
|                 .id(id) | ||||
|                 .label(label) | ||||
|                 .presentation(presentation) | ||||
|                 .lastQuizzDate(quizzes != null ? quizzes.stream().max(Comparator.comparing(PostgresQuizDto::getCreatedDate)).map(PostgresQuizDto::getCreatedDate).orElse(null): null) | ||||
|                 .numberOfQuizzes(quizzes != null ? quizzes.size(): 0) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         PostgresBundleDto axe = (PostgresBundleDto) o; | ||||
|         return id != null && Objects.equals(id, axe.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,74 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import jakarta.persistence.Column; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.JoinColumn; | ||||
| import jakarta.persistence.ManyToOne; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity(name = "question") | ||||
| public class PostgresQuestionDto { | ||||
|  | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "axe_id") | ||||
|     @ToString.Exclude | ||||
|     private PostgresAxeDto axe; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 200) | ||||
|     @Column(nullable = false) | ||||
|     private String label; | ||||
|  | ||||
|     @Size(max = 500) | ||||
|     private String description; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "bundle_id") | ||||
|     @ToString.Exclude | ||||
|     private PostgresBundleDto bundle; | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     private Integer index; | ||||
|  | ||||
|     public Question toQuestion() { | ||||
|         return Question.builder() | ||||
|                 .id(id) | ||||
|                 .axeId(axe.getId()) | ||||
|                 .label(label) | ||||
|                 .index(index) | ||||
|                 .description(description) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         var question = (PostgresQuestionDto) o; | ||||
|         return id != null && Objects.equals(id, question.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import jakarta.persistence.Column; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.EntityListeners; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.JoinColumn; | ||||
| import jakarta.persistence.ManyToOne; | ||||
| import jakarta.persistence.OneToMany; | ||||
| import lombok.Getter; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.springframework.data.annotation.CreatedDate; | ||||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||||
|  | ||||
| import java.util.Date; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @NoArgsConstructor | ||||
| @Entity(name = "quiz") | ||||
| @EntityListeners(AuditingEntityListener.class) | ||||
| public class PostgresQuizDto { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @Column(name = "created_date", nullable = false, updatable = false) | ||||
|     @CreatedDate | ||||
|     private Date createdDate; | ||||
|  | ||||
|     @OneToMany(mappedBy = "quiz") | ||||
|     @ToString.Exclude | ||||
|     private Set<PostgresQuizResponseDto> responses; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "bundle_id") | ||||
|     private PostgresBundleDto bundle; | ||||
|  | ||||
|     public Quiz toQuiz() { | ||||
|         var quiz = new Quiz(); | ||||
|         quiz.setId(id); | ||||
|         quiz.setCreatedDate(createdDate); | ||||
|         return quiz; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         PostgresQuizDto that = (PostgresQuizDto) o; | ||||
|         return id != null && Objects.equals(id, that.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,15 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.QuizResponse; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.JoinColumn; | ||||
| import jakarta.persistence.ManyToOne; | ||||
| import jakarta.persistence.OneToOne; | ||||
| import jakarta.validation.constraints.Max; | ||||
| import jakarta.validation.constraints.Min; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| @@ -9,14 +18,6 @@ import lombok.experimental.Accessors; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
| 
 | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.JoinColumn; | ||||
| import javax.persistence.ManyToOne; | ||||
| import javax.persistence.OneToOne; | ||||
| import javax.validation.constraints.Max; | ||||
| import javax.validation.constraints.Min; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| @Getter | ||||
| @@ -24,8 +25,8 @@ import java.util.Objects; | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Accessors(chain = true) | ||||
| @Entity | ||||
| public class Response { | ||||
| @Entity(name = "response") | ||||
| public class PostgresQuizResponseDto { | ||||
| 
 | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
| @@ -33,12 +34,12 @@ public class Response { | ||||
| 
 | ||||
|     @OneToOne | ||||
|     @JoinColumn(name = "question_id") | ||||
|     private Question question; | ||||
|     private PostgresQuestionDto question; | ||||
| 
 | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "quiz_id") | ||||
|     @JsonIgnore | ||||
|     private Quiz quiz; | ||||
|     private PostgresQuizDto quiz; | ||||
| 
 | ||||
|     @Min(0) | ||||
|     @Max(10) | ||||
| @@ -47,11 +48,19 @@ public class Response { | ||||
|     @Length(max = 500) | ||||
|     private String comment; | ||||
| 
 | ||||
|     public QuizResponse toQuizResponse() { | ||||
|         return new QuizResponse( | ||||
|                 getQuestion().getAxe().getIdentifier(), | ||||
|                 getQuestion().getLabel(), | ||||
|                 getScore(), | ||||
|                 getComment()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         Response response = (Response) o; | ||||
|         PostgresQuizResponseDto response = (PostgresQuizResponseDto) o; | ||||
|         return id != null && Objects.equals(id, response.id); | ||||
|     } | ||||
| 
 | ||||
| @@ -0,0 +1,66 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.RefreshToken; | ||||
| import jakarta.persistence.Column; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.JoinColumn; | ||||
| import jakarta.persistence.OneToOne; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
|  | ||||
| import java.time.Instant; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity(name = "refresh_token") | ||||
| public class PostgresRefreshTokenDto { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @OneToOne | ||||
|     @JoinColumn(name = "user_id", referencedColumnName = "id") | ||||
|     private PostgresUserDto user; | ||||
|  | ||||
|     @Column(nullable = false, unique = true) | ||||
|     private String token; | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     private Instant expiryDate; | ||||
|  | ||||
|     public PostgresRefreshTokenDto(RefreshToken refreshToken) { | ||||
|         this.id = refreshToken.getId(); | ||||
|         this.token = refreshToken.getToken(); | ||||
|         this.expiryDate = refreshToken.getExpiryDate(); | ||||
|     } | ||||
|  | ||||
|     public RefreshToken toRefreshToken() { | ||||
|         return RefreshToken.builder() | ||||
|                 .id(id) | ||||
|                 .user(user.toUser()) | ||||
|                 .token(token) | ||||
|                 .expiryDate(expiryDate) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         PostgresRefreshTokenDto that = (PostgresRefreshTokenDto) o; | ||||
|         return id != null && Objects.equals(id, that.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,87 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import jakarta.persistence.Column; | ||||
| import jakarta.persistence.Entity; | ||||
| import jakarta.persistence.EntityListeners; | ||||
| import jakarta.persistence.GeneratedValue; | ||||
| import jakarta.persistence.Id; | ||||
| import jakarta.persistence.Table; | ||||
| import jakarta.persistence.UniqueConstraint; | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.springframework.data.annotation.CreatedDate; | ||||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| @Table(name = "users", | ||||
|         uniqueConstraints = { | ||||
|                 @UniqueConstraint(columnNames = "username") | ||||
|         }) | ||||
| @EntityListeners(AuditingEntityListener.class) | ||||
| public class PostgresUserDto { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @Email | ||||
|     @Size(max = 100) | ||||
|     @NotBlank | ||||
|     @Column(unique = true, nullable = false, length = 100) | ||||
|     private String email; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 20) | ||||
|     private String username; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 120) | ||||
|     private String password; | ||||
|  | ||||
|     @Column(name = "created_date", nullable = false, updatable = false) | ||||
|     @CreatedDate | ||||
|     private LocalDateTime createdDate; | ||||
|  | ||||
|     public PostgresUserDto(User user) { | ||||
|         this.id = user.getId(); | ||||
|         this.email = user.getEmail(); | ||||
|         this.username = user.getUsername(); | ||||
|         this.password = user.getPassword(); | ||||
|     } | ||||
|  | ||||
|     public User toUser() { | ||||
|         return User.builder() | ||||
|                 .id(id) | ||||
|                 .email(email) | ||||
|                 .username(username) | ||||
|                 .password(password) | ||||
|                 .createdAt(createdDate) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         PostgresUserDto user = (PostgresUserDto) o; | ||||
|         return id != null && Objects.equals(id, user.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresAxeDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public interface AxeCachePostgresRepository extends Repository<PostgresAxeDto, Long> { | ||||
|     PostgresAxeDto findById(Long id); | ||||
|  | ||||
|     List<PostgresAxeDto> findAll(); | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Axe; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.spi.AxeCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresAxeDto; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class AxeCacheProxyRepository implements AxeCacheRepository { | ||||
|     private final AxeCachePostgresRepository axeCachePostgresRepository; | ||||
|  | ||||
|     public Iterable<Axe> findAll() { | ||||
|         return axeCachePostgresRepository.findAll() | ||||
|                 .stream() | ||||
|                 .map(PostgresAxeDto::toAxe) | ||||
|                 .toList(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresBundleDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface BundleCachePostgresRepository extends Repository<PostgresBundleDto, Long> { | ||||
|  | ||||
|     Optional<PostgresBundleDto> findByIdAndUserId(Long id, Long user_id); | ||||
|  | ||||
|     List<PostgresBundleDto> findAllByUserId(Long userId); | ||||
|  | ||||
|     PostgresBundleDto save(PostgresBundleDto postgresBundleDto); | ||||
| } | ||||
| @@ -0,0 +1,58 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Bundle; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.spi.BundleCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.UserCachePostgresRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresBundleDto; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuestionDto; | ||||
| import jakarta.transaction.Transactional; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.data.rest.webmvc.ResourceNotFoundException; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class BundleCacheProxyRepository implements BundleCacheRepository { | ||||
|  | ||||
|     private final BundleCachePostgresRepository bundleCachePostgresRepository; | ||||
|     private final QuestionCachePostgresRepository questionCachePostgresRepository; | ||||
|     private final AxeCachePostgresRepository axeCachePostgresRepository; | ||||
|     private final UserCachePostgresRepository userCachePostgresRepository; | ||||
|  | ||||
|     public Bundle findByIdAndUserId(Long id, Long userId) { | ||||
|         return bundleCachePostgresRepository.findByIdAndUserId(id, userId) | ||||
|                 .map(PostgresBundleDto::toBundle) | ||||
|                 .orElseThrow(() -> new ResourceNotFoundException("Bundle not found")); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<Bundle> findAllByUser(User user) { | ||||
|         return bundleCachePostgresRepository.findAllByUserId(user.getId()) | ||||
|                 .stream().map(PostgresBundleDto::toBundle) | ||||
|                 .toList(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public Bundle save(String label, String presentation, List<Question> questions, User user) { | ||||
|         var entity = new PostgresBundleDto(); | ||||
|         entity.setLabel(label); | ||||
|         entity.setPresentation(presentation); | ||||
|         entity.setUser(userCachePostgresRepository.findById(user.getId()).orElseThrow()); | ||||
|         var newEntity = bundleCachePostgresRepository.save(entity); | ||||
|         questions.forEach(question -> { | ||||
|             var questionEntity = new PostgresQuestionDto(); | ||||
|             questionEntity.setBundle(newEntity); | ||||
|             questionEntity.setDescription(question.getDescription()); | ||||
|             questionEntity.setLabel(question.getLabel()); | ||||
|             questionEntity.setAxe(axeCachePostgresRepository.findById(question.getAxeId())); | ||||
|             questionEntity.setIndex(question.getIndex()); | ||||
|             questionCachePostgresRepository.save(questionEntity); | ||||
|         }); | ||||
|         return newEntity.toBundle(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuestionDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface QuestionCachePostgresRepository extends Repository<PostgresQuestionDto, Long> { | ||||
|  | ||||
|     List<PostgresQuestionDto> findAllByAxeIdAndBundleIdOrderByIndexAsc(final Long axeId, final Long userId); | ||||
|  | ||||
|     List<PostgresQuestionDto> findAllByBundleIsNull(); | ||||
|  | ||||
|     List<PostgresQuestionDto> findAllByAxeIdAndBundleIsNullOrderByIndexAsc(final Long axeId); | ||||
|  | ||||
|     Optional<PostgresQuestionDto> findByIdAndBundleId(Long questionId, Long bundleId); | ||||
|  | ||||
|     PostgresQuestionDto save(PostgresQuestionDto entity); | ||||
|  | ||||
|     List<PostgresQuestionDto> findAllByAxeIdAndBundleIsNotNullOrderByIndexAsc(Long axeId); | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.spi.QuestionCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuestionDto; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class QuestionCacheProxyRepository implements QuestionCacheRepository { | ||||
|  | ||||
|     private final QuestionCachePostgresRepository questionCachePostgresRepository; | ||||
|  | ||||
|     public List<Question> findQuestions(Long axeId, Long bundleId) { | ||||
|         return questionCachePostgresRepository.findAllByAxeIdAndBundleIdOrderByIndexAsc(axeId, bundleId) | ||||
|                 .stream() | ||||
|                 .map(PostgresQuestionDto::toQuestion).collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     public List<Question> findDefaultQuestions() { | ||||
|         return questionCachePostgresRepository.findAllByBundleIsNull() | ||||
|                 .stream() | ||||
|                 .map(PostgresQuestionDto::toQuestion).collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     public List<Question> findDefaultQuestions(Long axeId) { | ||||
|         return questionCachePostgresRepository.findAllByAxeIdAndBundleIsNullOrderByIndexAsc(axeId) | ||||
|                 .stream() | ||||
|                 .map(PostgresQuestionDto::toQuestion).collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<Question> findAllByAxeId(Long axeId) { | ||||
|         var questions = questionCachePostgresRepository.findAllByAxeIdAndBundleIsNotNullOrderByIndexAsc(axeId) | ||||
|                 .stream() | ||||
|                 .map(PostgresQuestionDto::toQuestion).toList(); | ||||
|         Set<String> set = new HashSet<>(questions.size()); | ||||
|         return questions.stream().filter(q -> set.add(q.getLabel() + q.getDescription())).collect(Collectors.toList()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuizDto; | ||||
| import org.springframework.data.domain.Page; | ||||
| import org.springframework.data.domain.Pageable; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.nio.channels.FileChannel; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public interface QuizCachePostgresRepository extends Repository<PostgresQuizDto, Long> { | ||||
|  | ||||
|     Optional<PostgresQuizDto> findById(Long id); | ||||
|  | ||||
|     Page<PostgresQuizDto> findAllByBundleId(Long userId, Pageable pageable); | ||||
|  | ||||
|     List<PostgresQuizDto> findAllByCreatedDateBefore(Date createdDate); | ||||
|  | ||||
|     PostgresQuizDto save(PostgresQuizDto quiz); | ||||
|  | ||||
|     Optional<PostgresQuizDto> findByIdAndBundle_User_Id(Long id, Long userId); | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.spi.QuizCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuizDto; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.data.domain.Page; | ||||
| import org.springframework.data.domain.Pageable; | ||||
| import org.springframework.data.rest.webmvc.ResourceNotFoundException; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class QuizCacheProxyRepository implements QuizCacheRepository { | ||||
|  | ||||
|     private final QuizCachePostgresRepository quizCachePostgresRepository; | ||||
|     private final BundleCachePostgresRepository bundleCachePostgresRepository; | ||||
|  | ||||
|     public Optional<Quiz> findById(Long id) { | ||||
|         return quizCachePostgresRepository.findById(id).map(PostgresQuizDto::toQuiz); | ||||
|     } | ||||
|  | ||||
|     public Optional<Quiz> findByIdAndUserId(Long id, Long userId) { | ||||
|         return quizCachePostgresRepository.findByIdAndBundle_User_Id(id, userId).map(PostgresQuizDto::toQuiz); | ||||
|     } | ||||
|  | ||||
|     public Page<Quiz> findAllByBundleId(Long bundleId, Pageable pageable) { | ||||
|         return quizCachePostgresRepository.findAllByBundleId(bundleId, pageable).map(PostgresQuizDto::toQuiz); | ||||
|     } | ||||
|  | ||||
|     public Quiz createNew(Long bundleId, User user) { | ||||
|         var bundle = bundleCachePostgresRepository.findByIdAndUserId(bundleId, user.getId()) | ||||
|                 .orElseThrow(() -> new ResourceNotFoundException("Bundle not found")); | ||||
|         var quizDto = new PostgresQuizDto(); | ||||
|         quizDto.setBundle(bundle); | ||||
|         return quizCachePostgresRepository.save(quizDto).toQuiz(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuizResponseDto; | ||||
| import org.springframework.data.repository.Repository; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public interface ResponseCachePostgresRepository extends Repository<PostgresQuizResponseDto, Long> { | ||||
|  | ||||
|     PostgresQuizResponseDto save(PostgresQuizResponseDto quizResponseDto); | ||||
|  | ||||
|     List<PostgresQuizResponseDto> findAllByQuizIdOrderByQuestionIndexAsc(Long quizId); | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| package fr.itsonus.bousoleplussbackend.infrastructure.postgres.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.QuizResponse; | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.spi.ResponseCacheRepository; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuizResponseDto; | ||||
| import fr.itsonus.bousoleplussbackend.payload.request.ResponseRequest; | ||||
| import jakarta.transaction.Transactional; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.NoSuchElementException; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Service | ||||
| @AllArgsConstructor | ||||
| public class ResponseCacheProxyRepository implements ResponseCacheRepository { | ||||
|  | ||||
|     private final ResponseCachePostgresRepository responseCachePostgresRepository; | ||||
|     private final QuestionCachePostgresRepository questionCachePostgresRepository; | ||||
|     private final QuizCachePostgresRepository quizCachePostgresRepository; | ||||
|  | ||||
|     @Transactional | ||||
|     public void saveQuizzResponses(Long bundleId, Quiz quiz, Set<ResponseRequest> responses) { | ||||
|         var quizDto = quizCachePostgresRepository.findById(quiz.getId()).orElseThrow(); | ||||
|         responses.forEach(response -> { | ||||
|             var question = questionCachePostgresRepository.findByIdAndBundleId(response.getQuestionId(), bundleId) | ||||
|                     .orElseThrow(() -> new NoSuchElementException("No such question with id " + response.getQuestionId())); | ||||
|             responseCachePostgresRepository.save( | ||||
|                     new PostgresQuizResponseDto() | ||||
|                             .setScore(response.getScore()) | ||||
|                             .setComment(response.getComment()) | ||||
|                             .setQuiz(quizDto) | ||||
|                             .setQuestion(question)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public List<QuizResponse> findAllByQuizId(Long quizId) { | ||||
|         return responseCachePostgresRepository.findAllByQuizIdOrderByQuestionIndexAsc(quizId) | ||||
|                 .stream().map(PostgresQuizResponseDto::toQuizResponse).toList(); | ||||
|     } | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
|  | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.JoinColumn; | ||||
| import javax.persistence.ManyToOne; | ||||
| import javax.persistence.PrePersist; | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import javax.validation.constraints.Size; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| public class Question { | ||||
|  | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "axe_id") | ||||
|     @ToString.Exclude | ||||
|     private Axe axe; | ||||
|  | ||||
|     @JsonIgnore | ||||
|     private Long userId; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 200) | ||||
|     private String label; | ||||
|  | ||||
|     @Size(max = 500) | ||||
|     private String description; | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         var question = (Question) o; | ||||
|         return id != null && Objects.equals(id, question.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
|  | ||||
|     @PrePersist | ||||
|     public void prePersist() { | ||||
|         var userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | ||||
|         this.userId = userDetails.getId(); | ||||
|     } | ||||
| } | ||||
| @@ -1,70 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
| import org.springframework.data.annotation.CreatedDate; | ||||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
|  | ||||
| import javax.persistence.Column; | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.EntityListeners; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.OneToMany; | ||||
| import javax.persistence.PrePersist; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| @EntityListeners(AuditingEntityListener.class) | ||||
| public class Quiz { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @Column(name = "created_date", nullable = false, updatable = false) | ||||
|     @CreatedDate | ||||
|     private Date createdDate; | ||||
|  | ||||
|     @JsonIgnore | ||||
|     private Long userId; | ||||
|  | ||||
|     @OneToMany(mappedBy = "quiz") | ||||
|     @ToString.Exclude | ||||
|     private Set<Response> responses; | ||||
|  | ||||
|     @OneToMany(mappedBy = "quiz") | ||||
|     @ToString.Exclude | ||||
|     private List<QuizScore> scores; | ||||
|  | ||||
|     @PrePersist | ||||
|     public void prePersist() { | ||||
|         var userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | ||||
|         this.userId = userDetails.getId(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         Quiz that = (Quiz) o; | ||||
|         return id != null && Objects.equals(id, that.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.annotations.Immutable; | ||||
|  | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.JoinColumn; | ||||
| import javax.persistence.ManyToOne; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| @Immutable | ||||
| public class QuizScore { | ||||
|  | ||||
|     @Id | ||||
|     @JsonIgnore | ||||
|     private Long id; | ||||
|  | ||||
|     private Float scoreAvg; | ||||
|     private Integer axeIdentifier; | ||||
| //    @JsonIgnore | ||||
| //    private Long quizId; | ||||
|  | ||||
|     @ManyToOne | ||||
|     @JoinColumn(name = "quiz_id") | ||||
|     @JsonIgnore | ||||
|     private Quiz quiz; | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
|  | ||||
| import lombok.Data; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
|  | ||||
| import javax.persistence.Column; | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.JoinColumn; | ||||
| import javax.persistence.OneToOne; | ||||
| import java.time.Instant; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| public class RefreshToken { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @OneToOne | ||||
|     @JoinColumn(name = "user_id", referencedColumnName = "id") | ||||
|     private User user; | ||||
|  | ||||
|     @Column(nullable = false, unique = true) | ||||
|     private String token; | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     private Instant expiryDate; | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         RefreshToken that = (RefreshToken) o; | ||||
|         return id != null && Objects.equals(id, that.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -1,54 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.models; | ||||
|  | ||||
| import lombok.Data; | ||||
| import lombok.Getter; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.Setter; | ||||
| import lombok.ToString; | ||||
| import org.hibernate.Hibernate; | ||||
|  | ||||
| import javax.persistence.Entity; | ||||
| import javax.persistence.GeneratedValue; | ||||
| import javax.persistence.GenerationType; | ||||
| import javax.persistence.Id; | ||||
| import javax.persistence.Table; | ||||
| import javax.persistence.UniqueConstraint; | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import javax.validation.constraints.Size; | ||||
| import java.util.Objects; | ||||
|  | ||||
| @Getter | ||||
| @Setter | ||||
| @ToString | ||||
| @RequiredArgsConstructor | ||||
| @Entity | ||||
| @Table(name = "users", | ||||
|         uniqueConstraints = { | ||||
|                 @UniqueConstraint(columnNames = "username") | ||||
|         }) | ||||
| public class User { | ||||
|     @Id | ||||
|     @GeneratedValue | ||||
|     private Long id; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 20) | ||||
|     private String username; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(max = 120) | ||||
|     private String password; | ||||
|  | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; | ||||
|         User user = (User) o; | ||||
|         return id != null && Objects.equals(id, user.id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getClass().hashCode(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.quiz.model.Question; | ||||
| import jakarta.validation.Valid; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.NotEmpty; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public record BundleCreationRequest(@NotBlank @Length(max = 50) String label, @Length(max = 100) String presentation, | ||||
|                                     @Valid @NotEmpty List<QuestionCreationRequest> questions) { | ||||
|  | ||||
|     public record QuestionCreationRequest(@NotEmpty @Length(max = 200) String label, | ||||
|                                           @Length(max = 500) String description, | ||||
|                                           @NotNull Long axeId, | ||||
|                                           @NotNull Integer index) { | ||||
|         public Question toQuestion() { | ||||
|             return Question.builder().axeId(axeId).label(label).index(index).description(description).build(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,14 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.NotBlank; | ||||
|  | ||||
| @Data | ||||
| public class LoginRequest { | ||||
|     @NotBlank | ||||
|     private String username; | ||||
|     @Email | ||||
|     private String email; | ||||
|  | ||||
|     @NotBlank | ||||
|     private String password; | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import jakarta.validation.constraints.Email; | ||||
|  | ||||
| public record NotifyPasswordResetRequest(@Email String email) { | ||||
| } | ||||
| @@ -1,14 +1,12 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import lombok.Data; | ||||
| import jakarta.validation.Valid; | ||||
| import jakarta.validation.constraints.NotEmpty; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
|  | ||||
| import javax.validation.Valid; | ||||
| import javax.validation.constraints.NotEmpty; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Data | ||||
| public class QuizRequest { | ||||
|     @NotEmpty | ||||
|     private Set<@Valid ResponseRequest> responses; | ||||
| public record QuizRequest(@NotNull Long bundleId, | ||||
|                           @NotEmpty Set<@Valid ResponseRequest> responses) { | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,32 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.domain.auth.model.User; | ||||
| import fr.itsonus.bousoleplussbackend.payload.validation.Password; | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| @Data | ||||
| public class RegisterRequest { | ||||
|  | ||||
|     @NotBlank | ||||
|     @Email | ||||
|     private String email; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Length(min = 3, max = 20) | ||||
|     private String username; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Password | ||||
|     private String password; | ||||
|  | ||||
|     public User toUser() { | ||||
|         return User.builder() | ||||
|                 .email(email) | ||||
|                 .username(username) | ||||
|                 .password(password) | ||||
|                 .build(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.payload.validation.Password; | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
|  | ||||
| public record ResetPasswordRequest(@NotBlank String token, @Email String email, | ||||
|                                    @NotBlank @Password String newPassword, | ||||
|                                    @NotBlank String confirmationPassword) { | ||||
| } | ||||
| @@ -1,9 +1,8 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.NotNull; | ||||
|  | ||||
| @Data | ||||
| public class ResponseRequest { | ||||
|     @NotNull | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import javax.validation.constraints.Size; | ||||
|  | ||||
| @Data | ||||
| public class SignupRequest { | ||||
|     @NotBlank | ||||
|     @Size(min = 3, max = 20) | ||||
|     private String username; | ||||
|  | ||||
|     @NotBlank | ||||
|     @Size(min = 6, max = 40) | ||||
|     private String password; | ||||
| } | ||||
| @@ -1,9 +1,8 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.NotBlank; | ||||
|  | ||||
| @Data | ||||
| public class TokenRefreshRequest { | ||||
|     @NotBlank | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import jakarta.validation.constraints.Email; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
|  | ||||
| public record UpdateAccountRequest(@NotBlank String username, @NotNull @Email String email) { | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.request; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.payload.validation.Password; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
|  | ||||
| public record UpdatePasswordRequest(@NotBlank String currentPassword, | ||||
|                                     @NotBlank @Password String newPassword, | ||||
|                                     @NotBlank String confirmationPassword) { | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.response; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.http.HttpStatusCode; | ||||
| import org.springframework.http.ResponseEntity; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public record ApiError(String message, @JsonInclude(JsonInclude.Include.NON_EMPTY) List<FieldError> fieldErrors) { | ||||
|  | ||||
|     public ApiError(String message) { | ||||
|         this(message, List.of()); | ||||
|     } | ||||
|  | ||||
|     public ResponseEntity<ApiError> toResponse(HttpStatus status) { | ||||
|         return ResponseEntity.status(status).body(this); | ||||
|     } | ||||
|  | ||||
|     public ResponseEntity<ApiError> toResponse(HttpStatusCode status) { | ||||
|         return ResponseEntity.status(status).body(this); | ||||
|     } | ||||
|  | ||||
|     public record FieldError(String detail, @JsonInclude(JsonInclude.Include.NON_EMPTY) String... fields) { | ||||
|     } | ||||
| } | ||||
| @@ -1,15 +1,17 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.response; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Question; | ||||
| import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuestionDto; | ||||
| import lombok.Data; | ||||
| import lombok.EqualsAndHashCode; | ||||
| import org.springframework.hateoas.RepresentationModel; | ||||
|  | ||||
| @Data | ||||
| @EqualsAndHashCode(callSuper = true) | ||||
| public class AxeWithQuestion extends RepresentationModel<AxeWithQuestion> { | ||||
|  | ||||
|     private Integer identifier; | ||||
|     private String shortTitle; | ||||
|     private String title; | ||||
|     private String color; | ||||
|     private Iterable<Question> questions; | ||||
|     private Iterable<PostgresQuestionDto> questions; | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.response; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.Token; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
|  | ||||
| @Data | ||||
| @AllArgsConstructor | ||||
| public class JwtResponse { | ||||
|  | ||||
|     private static String type = "Bearer"; | ||||
|     private String token; | ||||
|     private Token token; | ||||
|     private String refreshToken; | ||||
|     private Long id; | ||||
|     private String username; | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import lombok.Data; | ||||
| @AllArgsConstructor | ||||
| public class TokenRefreshResponse { | ||||
|  | ||||
|     private static String tokenType = "Bearer"; | ||||
|     private String tokenType; | ||||
|     private String accessToken; | ||||
|     private String refreshToken; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.validation; | ||||
|  | ||||
| import jakarta.validation.Constraint; | ||||
| import jakarta.validation.Payload; | ||||
|  | ||||
| import java.lang.annotation.Documented; | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
|  | ||||
| @Documented | ||||
| @Constraint(validatedBy = PasswordConstraintValidator.class) | ||||
| @Target( { ElementType.METHOD, ElementType.FIELD }) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface Password { | ||||
|     String message() default "Votre mot de passe doit fait au moins 8 caractères, et être composé d’au moins une majuscule, une minuscule, un chiffre de 0 à 9, et un caractère spécial parmi @?!#$&;,:"; | ||||
|     Class<?>[] groups() default {}; | ||||
|     Class<? extends Payload>[] payload() default {}; | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package fr.itsonus.bousoleplussbackend.payload.validation; | ||||
|  | ||||
| import jakarta.validation.ConstraintValidator; | ||||
| import jakarta.validation.ConstraintValidatorContext; | ||||
|  | ||||
| public class PasswordConstraintValidator implements ConstraintValidator<Password, String> { | ||||
|  | ||||
|     @Override | ||||
|     public boolean isValid(String password, ConstraintValidatorContext cxt) { | ||||
|         return password != null && password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@?!#$&;,:])[A-Za-z\\d@?!#$&;,:]{8,}$"); | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.projections; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Question; | ||||
| import org.springframework.data.rest.core.config.Projection; | ||||
|  | ||||
| @Projection(types = {Question.class}) | ||||
| public interface QuestionProj { | ||||
|     Long getId(); | ||||
|     String getLabel(); | ||||
|     String getDescription(); | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.projections; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.models.Response; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.data.rest.core.config.Projection; | ||||
|  | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
|  | ||||
| @Projection(name = "responseWithQuestion", types = { Quiz.class }) | ||||
| public interface QuizWithResponses { | ||||
|  | ||||
|     String getCreatedDate(); | ||||
|  | ||||
|     @Value("#{target.getResponses()}") | ||||
|     Set<ResponseWithQuestion> getResponses(); | ||||
|  | ||||
| //    @Value("#{target.getResponses().stream().collect(Collectors.toMap(value -> value, value -> value.length()))}") | ||||
| //    Map<Integer, ResponseWithQuestion> getResponses(); | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.projections; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.models.QuizScore; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.data.rest.core.config.Projection; | ||||
|  | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
|  | ||||
| @Projection(name = "quizWithScore", types = {Quiz.class}) | ||||
| public interface QuizWithScore { | ||||
|  | ||||
|     Long getId(); | ||||
|  | ||||
|     Date getCreatedDate(); | ||||
|  | ||||
|     @Value("#{target.getScores()}") | ||||
|     List<QuizScore> getScores(); | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.projections; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Response; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.data.rest.core.config.Projection; | ||||
|  | ||||
| @Projection(name = "responseWithQuestion", types = { Response.class }) | ||||
| public interface ResponseWithQuestion { | ||||
|  | ||||
|     String getComment(); | ||||
|     Short getScore(); | ||||
|  | ||||
|     @Value("#{target.getQuestion().getLabel()}") | ||||
|     String getQuestion(); | ||||
|  | ||||
|     @Value("#{target.getQuestion().getAxe().getIdentifier()}") | ||||
|     Integer getAxeIdentifier(); | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Axe; | ||||
| import org.springframework.data.jpa.repository.JpaSpecificationExecutor; | ||||
| import org.springframework.data.repository.CrudRepository; | ||||
| import org.springframework.data.rest.core.annotation.RepositoryRestResource; | ||||
|  | ||||
| @RepositoryRestResource | ||||
| public interface AxeRepository extends CrudRepository<Axe, Long>, JpaSpecificationExecutor<Axe> { | ||||
|  | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Question; | ||||
| import org.springframework.data.jpa.repository.Query; | ||||
| import org.springframework.data.repository.CrudRepository; | ||||
| import org.springframework.data.repository.query.Param; | ||||
| import org.springframework.data.rest.core.annotation.RepositoryRestResource; | ||||
| import org.springframework.data.rest.core.annotation.RestResource; | ||||
|  | ||||
| @RepositoryRestResource | ||||
| public interface QuestionRepository extends CrudRepository<Question, Long> { | ||||
|  | ||||
|     @RestResource(path="byAxeId", rel="byAxeId") | ||||
|     @Query("SELECT q FROM Question q JOIN q.axe a WHERE q.axe.id = :id AND user_id = ?#{principal.id}") | ||||
|     Iterable<Question> findAllByAxeId(@Param("id") final Long id); | ||||
|  | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Quiz; | ||||
| import fr.itsonus.bousoleplussbackend.projections.QuizWithScore; | ||||
| import org.springframework.data.domain.Page; | ||||
| import org.springframework.data.domain.Pageable; | ||||
| import org.springframework.data.jpa.repository.Query; | ||||
| import org.springframework.data.repository.PagingAndSortingRepository; | ||||
| import org.springframework.data.rest.core.annotation.RepositoryRestResource; | ||||
| import org.springframework.data.rest.core.annotation.RestResource; | ||||
|  | ||||
| @RepositoryRestResource(excerptProjection = QuizWithScore.class) | ||||
| public interface QuizRepository extends PagingAndSortingRepository<Quiz, Long> { | ||||
|  | ||||
|     @RestResource(path = "me", rel = "me") | ||||
|     @Query("SELECT q FROM Quiz q WHERE q.userId = ?#{principal.id}") | ||||
|     Page<Quiz> findAllWithScoresOfCurrentUser(final Pageable pageable); | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.QuizScore; | ||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||
| import org.springframework.stereotype.Repository; | ||||
|  | ||||
| @Repository | ||||
| public interface QuizScoreRepository extends JpaRepository<QuizScore, Long> { | ||||
|  | ||||
|     Iterable<QuizScore> findAllByQuizId(Long quizId); | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.RefreshToken; | ||||
| import fr.itsonus.bousoleplussbackend.models.User; | ||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||
| import org.springframework.data.jpa.repository.Modifying; | ||||
| import org.springframework.stereotype.Repository; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Repository | ||||
| public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { | ||||
|     Optional<RefreshToken> findByToken(String token); | ||||
|  | ||||
|     @Modifying | ||||
|     int deleteByUser(User user); | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.Response; | ||||
| import fr.itsonus.bousoleplussbackend.projections.ResponseWithQuestion; | ||||
| import org.springframework.data.repository.CrudRepository; | ||||
| import org.springframework.data.rest.core.annotation.RepositoryRestResource; | ||||
|  | ||||
| @RepositoryRestResource(excerptProjection = ResponseWithQuestion.class) | ||||
| public interface ResponseRepository extends CrudRepository<Response, Long> { | ||||
|  | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.repositories; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.models.User; | ||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||
| import org.springframework.stereotype.Repository; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Repository | ||||
| public interface UserRepository extends JpaRepository<User, Long> { | ||||
|     Optional<User> findByUsername(String username); | ||||
|  | ||||
|     Boolean existsByUsername(String username); | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security; | ||||
|  | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.core.annotation.Order; | ||||
| import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; | ||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; | ||||
| import org.springframework.web.cors.CorsConfiguration; | ||||
| import org.springframework.web.cors.CorsConfigurationSource; | ||||
| import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @Configuration | ||||
| @EnableWebSecurity | ||||
| @EnableMethodSecurity(securedEnabled = true) | ||||
| @RequiredArgsConstructor | ||||
| @Order(1) | ||||
| public class CoreSecurityConfig { | ||||
|  | ||||
|     @Value("${security.cors.allow-origins}") | ||||
|     private List<String> corsOrigins; | ||||
|  | ||||
|     @Bean | ||||
|     public PasswordEncoder passwordEncoder() { | ||||
|         return new BCryptPasswordEncoder(); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     CorsConfigurationSource corsConfigurationSource() { | ||||
|         var configuration = new CorsConfiguration(); | ||||
|         configuration.setAllowedOrigins(corsOrigins); | ||||
|         configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "OPTIONS")); | ||||
|         configuration.setAllowedHeaders(List.of("*")); | ||||
|         var source = new UrlBasedCorsConfigurationSource(); | ||||
|         source.registerCorsConfiguration("/**", configuration); | ||||
|         return source; | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public SecurityEvaluationContextExtension securityEvaluationContextExtension() { | ||||
|         return new SecurityEvaluationContextExtension(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,67 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.ExceptionHandlerFilter; | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.JwtAuthenticationFilter; | ||||
| import fr.itsonus.bousoleplussbackend.security.jwt.JwtGenerator; | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsServiceImpl; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.http.HttpMethod; | ||||
| import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||||
| import org.springframework.security.config.Customizer; | ||||
| 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.password.PasswordEncoder; | ||||
| import org.springframework.security.web.SecurityFilterChain; | ||||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||||
| import org.springframework.security.web.header.HeaderWriterFilter; | ||||
| import org.springframework.web.cors.CorsConfigurationSource; | ||||
|  | ||||
| @Configuration | ||||
| @EnableWebSecurity | ||||
| @EnableMethodSecurity(securedEnabled = true) | ||||
| @RequiredArgsConstructor | ||||
| public class SecurityConfig { | ||||
|  | ||||
|     private final UserDetailsServiceImpl userDetailsService; | ||||
|     private final PasswordEncoder passwordEncoder; | ||||
|     private final CorsConfigurationSource corsConfigurationSource; | ||||
|     private final JwtGenerator jwtGenerator; | ||||
|     private final ExceptionHandlerFilter exceptionHandlerFilter; | ||||
|  | ||||
|     @Bean | ||||
|     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||||
|         http | ||||
|                 .cors(cors -> cors.configurationSource(corsConfigurationSource)) | ||||
|                 .csrf(AbstractHttpConfigurer::disable) | ||||
|                 .sessionManagement(Customizer.withDefaults()) | ||||
|                 .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||||
|                 .securityMatcher("/auth/**", "/account/**", "bundles/**", "/quizzes/**", "/axes/**", "questions/**", "/responses/**") | ||||
|                 .authorizeHttpRequests(matcher -> matcher | ||||
|                         .requestMatchers(HttpMethod.POST, | ||||
|                                 "/auth/login", "/auth/register", "/auth/refresh-token", | ||||
|                                 "/account/password/notify-reset-request", "/account/password/reset") | ||||
|                         .permitAll() | ||||
|                         .anyRequest().authenticated()) | ||||
|                 .httpBasic(Customizer.withDefaults()) | ||||
|                 .addFilterBefore(exceptionHandlerFilter, HeaderWriterFilter.class) | ||||
|                 .addFilterBefore(userJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); | ||||
|         return http.build(); | ||||
|     } | ||||
|  | ||||
|     public JwtAuthenticationFilter userJwtAuthenticationFilter() { | ||||
|         return new JwtAuthenticationFilter(jwtGenerator, userDetailsService); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public DaoAuthenticationProvider userAuthenticationProvider() { | ||||
|         DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); | ||||
|         provider.setUserDetailsService(userDetailsService); | ||||
|         provider.setPasswordEncoder(passwordEncoder); | ||||
|         return provider; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security; | ||||
|  | ||||
| import javax.crypto.SecretKey; | ||||
| import javax.crypto.spec.SecretKeySpec; | ||||
| import java.math.BigInteger; | ||||
| import java.security.SecureRandom; | ||||
| import java.util.Base64; | ||||
|  | ||||
| public class SecurityConstants { | ||||
|     public static final SecretKey JWT_SECRET; | ||||
|  | ||||
|     static { | ||||
|         var randomGenerator = new SecureRandom(); | ||||
|         byte[] randomBytes = new byte[128]; | ||||
|         randomGenerator.nextBytes(randomBytes); | ||||
|         JWT_SECRET = new SecretKeySpec( | ||||
|                 Base64.getDecoder().decode(new BigInteger(1, randomBytes).toString(16)), | ||||
|                 "HmacSHA256"); | ||||
|     } | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
|  | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| import org.springframework.security.web.AuthenticationEntryPoint; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
|  | ||||
| @Component | ||||
| public class AuthEntryPointJwt implements AuthenticationEntryPoint { | ||||
|  | ||||
|   private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); | ||||
|  | ||||
|   @Override | ||||
|   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) | ||||
|       throws IOException, ServletException { | ||||
|     logger.error("Unauthorized error: {}", authException.getMessage()); | ||||
|  | ||||
|     response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||||
|     response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||||
|  | ||||
|     final Map<String, Object> body = new HashMap<>(); | ||||
|     body.put("status", HttpServletResponse.SC_UNAUTHORIZED); | ||||
|     body.put("error", "Unauthorized"); | ||||
|     body.put("message", authException.getMessage()); | ||||
|     body.put("path", request.getServletPath()); | ||||
|  | ||||
|     final ObjectMapper mapper = new ObjectMapper(); | ||||
|     mapper.writeValue(response.getOutputStream(), body); | ||||
|  | ||||
| //    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsServiceImpl; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 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.util.StringUtils; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
|  | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| import java.io.IOException; | ||||
|  | ||||
| @Slf4j | ||||
| public class AuthTokenFilter extends OncePerRequestFilter { | ||||
|     @Autowired | ||||
|     private JwtUtils jwtUtils; | ||||
|  | ||||
|     @Autowired | ||||
|     private UserDetailsServiceImpl userDetailsService; | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws ServletException, IOException { | ||||
|         try { | ||||
|             var jwt = parseJwt(request); | ||||
|             if (jwt != null && jwtUtils.validateJwtToken(jwt)) { | ||||
|                 String username = jwtUtils.getUserNameFromJwtToken(jwt); | ||||
|  | ||||
|                 UserDetails userDetails = userDetailsService.loadUserByUsername(username); | ||||
|                 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, | ||||
|                         userDetails.getAuthorities()); | ||||
|                 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||||
|  | ||||
|                 SecurityContextHolder.getContext().setAuthentication(authentication); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             log.error("Cannot set user authentication: {}", e.getMessage()); | ||||
|         } | ||||
|  | ||||
|         filterChain.doFilter(request, response); | ||||
|     } | ||||
|  | ||||
|     private String parseJwt(HttpServletRequest request) { | ||||
|         var headerAuth = request.getHeader("Authorization"); | ||||
|         if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { | ||||
|             return headerAuth.substring(7); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import jakarta.servlet.FilterChain; | ||||
| import jakarta.servlet.ServletException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| @Component | ||||
| public class ExceptionHandlerFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||||
|         try { | ||||
|             filterChain.doFilter(request, response); | ||||
|         } catch (AuthenticationException e) { | ||||
|             response.sendError(HttpStatus.UNAUTHORIZED.value(), e.getMessage()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,50 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import jakarta.servlet.FilterChain; | ||||
| import jakarta.servlet.ServletException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| @AllArgsConstructor | ||||
| public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     private JwtGenerator jwtGenerator; | ||||
|     private UserDetailsService userDetailsService; | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws ServletException, IOException { | ||||
|         var token = getJWTFromRequest(request); | ||||
|  | ||||
|         if (token != null && jwtGenerator.validateToken(token)) { | ||||
|             String subject = jwtGenerator.getSubjectFromJWT(token); | ||||
|             UserDetails userDetails = userDetailsService.loadUserByUsername(subject); | ||||
|             UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( | ||||
|                     userDetails, | ||||
|                     null, userDetails.getAuthorities()); | ||||
|  | ||||
|             authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||||
|             SecurityContextHolder.getContext().setAuthentication(authenticationToken); | ||||
|         } | ||||
|         filterChain.doFilter(request, response); | ||||
|     } | ||||
|  | ||||
|     private String getJWTFromRequest(HttpServletRequest request) { | ||||
|         String bearerToken = request.getHeader("Authorization"); | ||||
|         if (bearerToken != null && bearerToken.startsWith("Bearer ")) { | ||||
|             return bearerToken.substring(7); | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,71 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.SecurityConstants; | ||||
| import io.jsonwebtoken.ExpiredJwtException; | ||||
| import io.jsonwebtoken.Jwts; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| @Component | ||||
| public class JwtGenerator { | ||||
|  | ||||
|     @Value("${security.jwt.auth-expiration}") | ||||
|     private int jwtAuthExpirationMs; | ||||
|  | ||||
|     @Value("${security.jwt.refresh-expiration}") | ||||
|     private int jwtRefreshExpirationMs; | ||||
|  | ||||
|     @Value("${security.jwt.reset-password-link-expiration}") | ||||
|     private int jwtResetPasswordLinkExpirationMs; | ||||
|  | ||||
|     public Token generateToken(String subject, long jwtExpirationMs) { | ||||
|         var currentDate = new Date(); | ||||
|         var expiryDate = new Date(currentDate.getTime() + jwtExpirationMs); | ||||
|         var token = Jwts.builder() | ||||
|                 .subject(subject) | ||||
|                 .issuedAt(currentDate) | ||||
|                 .expiration(expiryDate) | ||||
|                 .signWith(SecurityConstants.JWT_SECRET) | ||||
|                 .compact(); | ||||
|         return new Token("Bearer", token, currentDate.toInstant(), expiryDate.toInstant()); | ||||
|     } | ||||
|  | ||||
|     public Token generateResetPasswordToken(String subject) { | ||||
|         return generateToken(subject, jwtResetPasswordLinkExpirationMs); | ||||
|     } | ||||
|  | ||||
|     public Token generateAuthToken(Authentication authentication) { | ||||
|         return generateToken(authentication.getName(), jwtAuthExpirationMs); | ||||
|     } | ||||
|  | ||||
|     public Token generateRefreshToken(String subject) { | ||||
|         return generateToken(subject, jwtRefreshExpirationMs); | ||||
|     } | ||||
|  | ||||
|     public String getSubjectFromJWT(String token) { | ||||
|         var builder = Jwts.parser() | ||||
|                 .verifyWith(SecurityConstants.JWT_SECRET) | ||||
|                 .build(); | ||||
|         var claims = builder.parseSignedClaims(token) | ||||
|                 .getPayload(); | ||||
|         return claims.getSubject(); | ||||
|     } | ||||
|  | ||||
|     public boolean validateToken(String token) { | ||||
|         try { | ||||
|             Jwts.parser() | ||||
|                     .verifyWith(SecurityConstants.JWT_SECRET) | ||||
|                     .build() | ||||
|                     .parseSignedClaims(token); | ||||
|             return true; | ||||
|         } catch (ExpiredJwtException ex) { | ||||
|             throw new AuthenticationCredentialsNotFoundException("JWT token is expired"); | ||||
|         } catch (Exception ex) { | ||||
|             throw new AuthenticationCredentialsNotFoundException("JWT token is not valid"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,62 +0,0 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl; | ||||
| import io.jsonwebtoken.ExpiredJwtException; | ||||
| import io.jsonwebtoken.Jwts; | ||||
| import io.jsonwebtoken.MalformedJwtException; | ||||
| import io.jsonwebtoken.SignatureAlgorithm; | ||||
| import io.jsonwebtoken.SignatureException; | ||||
| import io.jsonwebtoken.UnsupportedJwtException; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| @Slf4j | ||||
| @Component | ||||
| public class JwtUtils { | ||||
|     private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); | ||||
|  | ||||
|     @Value("${app.jwtSecret}") | ||||
|     private String jwtSecret; | ||||
|  | ||||
|     @Value("${app.jwtExpirationMs}") | ||||
|     private int jwtExpirationMs; | ||||
|  | ||||
|     public String generateJwtToken(UserDetailsImpl userPrincipal) { | ||||
|         return generateTokenFromUsername(userPrincipal.getUsername()); | ||||
|     } | ||||
|  | ||||
|     public String generateTokenFromUsername(String username) { | ||||
|         return Jwts.builder().setSubject(username).setIssuedAt(new Date()) | ||||
|                 .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)).signWith(SignatureAlgorithm.HS512, jwtSecret) | ||||
|                 .compact(); | ||||
|     } | ||||
|  | ||||
|     public String getUserNameFromJwtToken(String token) { | ||||
|         return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject(); | ||||
|     } | ||||
|  | ||||
|     public boolean validateJwtToken(String authToken) { | ||||
|         try { | ||||
|             Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| package fr.itsonus.bousoleplussbackend.security.jwt; | ||||
|  | ||||
| import java.time.Instant; | ||||
|  | ||||
| public record Token(String type, String value, Instant createdAt, Instant expireAt) { | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user