Merge pull request 'feat: review backend and frontend' (#3) from feat/v2 into main
Reviewed-on: #3main 2.0.0
commit
b4c1348e3e
|
@ -0,0 +1,14 @@
|
|||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=securePassword123
|
||||
DATABASE_HOST=database
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_NAME=pluss_db
|
||||
|
||||
MAIL_HOST=TO_FILL#exemple: mail.apes-hdf.org
|
||||
MAIL_PORT=TO_FILL#exemple:587
|
||||
MAIL_FROM=TO_FILL#exemple:ne-pas-repondre@apes-hdf.org
|
||||
MAIL_USERNAME=TO_FILL#exemple:ne-pas-repondre@apes-hdf.org
|
||||
MAIL_PASSWORD=TO_FILL
|
||||
MAIL_ACTIVATE_DEBUG=false# set to true to debug mailing
|
||||
|
||||
FRONTEND_URL=http://localhost:8190/#exemple: https://pluss.apes.fr
|
|
@ -4,6 +4,9 @@
|
|||
/frontend/.eslintcache
|
||||
/frontend/dist
|
||||
/frontend/coverage
|
||||
/frontend/yarn.lock
|
||||
|
||||
**/.env
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
|
79
README.md
79
README.md
|
@ -1,15 +1,32 @@
|
|||
# Boussole PLUSS
|
||||
|
||||
Le projet backend contient la partie backend de la Boussole PLUSS. Le projet frontend, le frontend communiquant avec le backend.
|
||||
## Contribuer à la Boussole
|
||||
Le projet actuel est prévu pour travailler sur le thème de la production locale.
|
||||
Cet outil peut servir à évaluer des collectifs sur d’autres thématiques. Vous êtes encouragé à y réfléchir et à y
|
||||
travailler. La licence AGPL version 3 prévoit un partage des améliorations que vous porterez à l’outil. Si vous
|
||||
désirez faire un fork de l’outil, contactez-nous : contact@apes-hdf.org
|
||||
|
||||
## Installation
|
||||
|
||||
Le projet backend contient la partie backend de la Boussole PLUSS. Le projet frontend, le site web communiquant avec le backend.
|
||||
|
||||
|
||||
Copier/coller le fichier `.env_template` en `.env` et modifier les variables d'environnements.
|
||||
|
||||
En utilisant docker-compose :
|
||||
`docker-compose build` en modifiant au préalable l'argument BACKEND_BASE_URL du fichier
|
||||
docker-compose.yml pour spécifier l'URL sur laquelle pointera le backend.
|
||||
`docker-compose up` pour lancer le frontend et backend.
|
||||
|
||||
Exemple de configuration Nginx compatible avec le docker-compose de ce projet (URL_EXTERNE est à remplacer
|
||||
Au premier lancement, il faut initialiser la base de donnée. Pour ce faire :
|
||||
|
||||
- `docker-compose up -d database`
|
||||
- `docker exec database psql -U postgres -c "CREATE DATABASE pluss_db ENCODING UTF8" postgres`
|
||||
|
||||
En suite, les autres services peuvent être lancés avec `docker-compose up -d`. La base de données sera remplie automatiquement par le service backend.
|
||||
|
||||
Le site est accessible via le service `frontend`, dans cet exemple du docker-compose.yml, depuis http://localhost:8190.
|
||||
|
||||
Voici un exemple de configuration Nginx compatible avec le docker-compose de ce projet (URL_EXTERNE est à remplacer
|
||||
par l'URL publique) :
|
||||
|
||||
```
|
||||
|
@ -46,59 +63,3 @@ server {
|
|||
```
|
||||
|
||||
Ceci est un exemple à ne pas utiliser sur un environnement de production.
|
||||
|
||||
## Administration
|
||||
|
||||
Par la suite, `__URL__` est l'URL du backend, `__USER__` et `__PASSWORD__` le nom de compte et mot de passe à créer / utiliser.
|
||||
### Créer un nouveau compte
|
||||
```bash
|
||||
curl --location --request POST '__URL__/api/auth/signup' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"username": "__USER__",
|
||||
"password": "__PASSWORD__"
|
||||
}'
|
||||
```
|
||||
|
||||
### Se connecter
|
||||
```bash
|
||||
curl --location --request POST '__URL__/api/auth/signin' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"username": "__USER__",
|
||||
"password": "__PASSWORD__"
|
||||
}'
|
||||
```
|
||||
Cet appel renverra un token à réutiliser pour l'ensemble des requêtes. Par exemple :
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkZW1vIiwiaWF0IjoxNjY1NjY5NjUyLCJleHAiOjE2NjU2NzMyNTJ9.i426thZKL4-JvOt9ZeG2D1O6O-xlSiPoCMiKysHYzCkHVNrnxKetq8xKRNCTnbpLV-wagpOw2g-om34k2jtHIw",
|
||||
"refreshToken": "de73adcb-a5a8-4675-8bb3-5536651be0f9",
|
||||
"id": 11,
|
||||
"username": "demo"
|
||||
}
|
||||
```
|
||||
|
||||
### Afficher les balises (axe)
|
||||
```bash
|
||||
curl --location --request GET '__URL__/api/axes' \
|
||||
--header 'Authorization: Bearer TOKEN'
|
||||
```
|
||||
Le TOKEN est à remplacer par celui reçu.
|
||||
|
||||
### Ajouter une question à un axe
|
||||
```bash
|
||||
curl --location --request POST '__URL__/api/questions' \
|
||||
--header 'Authorization: Bearer TOKEN' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '
|
||||
{
|
||||
"axe": "URL_BALISE_1",
|
||||
"label": "Question 1",
|
||||
"description": "Description .."
|
||||
}
|
||||
'
|
||||
```
|
||||
Il faut spécifier URL_BALISE_1 comme étant l'URL de la balise 1 récupérer dans la requête précédente.
|
||||
Par exemple : `__URL__/axes/1`
|
|
@ -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!"));
|
||||
@PostMapping("/register")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void registerUser(@Valid @RequestBody RegisterRequest request) {
|
||||
userCreationUseCase.createUserAndDefaultBundle(request.toUser());
|
||||
}
|
||||
|
||||
// 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("/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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue