Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

207 changed files with 40488 additions and 5584 deletions

View File

@ -1,14 +0,0 @@
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

3
.gitignore vendored
View File

@ -4,9 +4,6 @@
/frontend/.eslintcache /frontend/.eslintcache
/frontend/dist /frontend/dist
/frontend/coverage /frontend/coverage
/frontend/yarn.lock
**/.env
# Node dependencies # Node dependencies
node_modules node_modules

View File

@ -1,32 +1,15 @@
# Boussole PLUSS # Boussole PLUSS
## Contribuer à la Boussole Le projet backend contient la partie backend de la Boussole PLUSS. Le projet frontend, le frontend communiquant avec le backend.
Le projet actuel est prévu pour travailler sur le thème de la production locale.
Cet outil peut servir à évaluer des collectifs sur dautres 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 à loutil. Si vous
désirez faire un fork de loutil, contactez-nous : contact@apes-hdf.org
## Installation ## Installation
Le projet backend contient la partie backend de la Boussole PLUSS. Le projet frontend, le site web communiquant avec le backend. En utilisant docker-compose:
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 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.yml pour spécifier l'URL sur laquelle pointera le backend.
`docker-compose up` pour lancer le frontend et backend.
Au premier lancement, il faut initialiser la base de donnée. Pour ce faire : Exemple de configuration Nginx compatible avec le docker-compose de ce projet (URL_EXTERNE est à remplacer
- `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) : par l'URL publique) :
``` ```
@ -63,3 +46,59 @@ server {
``` ```
Ceci est un exemple à ne pas utiliser sur un environnement de production. 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`

View File

@ -1,14 +0,0 @@
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/

View File

@ -1,4 +1,4 @@
FROM maven:3.9.8-eclipse-temurin-21 as builder FROM maven:3.8.6-eclipse-temurin-17 as builder
WORKDIR /src WORKDIR /src
COPY . /src COPY . /src
@ -8,7 +8,7 @@ WORKDIR /app
RUN cp /src/target/bousole-pluss-backend*.jar bousole-pluss-backend.jar RUN cp /src/target/bousole-pluss-backend*.jar bousole-pluss-backend.jar
RUN java -Djarmode=layertools -jar bousole-pluss-backend.jar extract RUN java -Djarmode=layertools -jar bousole-pluss-backend.jar extract
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:17-jre-alpine
EXPOSE 8080 EXPOSE 8080
USER root USER root
@ -18,4 +18,4 @@ COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/snapshot-dependencies/ ./ COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./ COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/application/ ./ COPY --from=builder /app/application/ ./
ENTRYPOINT java org.springframework.boot.loader.launch.JarLauncher ENTRYPOINT java org.springframework.boot.loader.JarLauncher

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.7</version> <version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>fr.itsonus</groupId> <groupId>fr.itsonus</groupId>
@ -14,9 +14,9 @@
<name>bousole-pluss-backend</name> <name>bousole-pluss-backend</name>
<description>Backend projet of Bousole PLUSS project</description> <description>Backend projet of Bousole PLUSS project</description>
<properties> <properties>
<java.version>21</java.version> <java.version>17</java.version>
<jjwt.version>0.12.6</jjwt.version> <jjwt.version>0.9.1</jjwt.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@ -31,6 +31,10 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId> <artifactId>spring-security-data</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@ -48,33 +52,15 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>spring-boot-starter-mail</artifactId> <artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>com.h2database</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>h2</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> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -2,11 +2,6 @@ package fr.itsonus.bousoleplussbackend;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; 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 @SpringBootApplication
public class Application { public class Application {
@ -14,11 +9,4 @@ public class Application {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(Application.class, args); SpringApplication.run(Application.class, args);
} }
@Bean
public LocaleResolver localeResolver() {
var slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.FRANCE);
return slr;
}
} }

View File

@ -0,0 +1,72 @@
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);
}
}

View File

@ -1,45 +0,0 @@
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());
}
}

View File

@ -1,85 +1,101 @@
package fr.itsonus.bousoleplussbackend.controllers; 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.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.LogOutRequest;
import fr.itsonus.bousoleplussbackend.payload.request.LoginRequest; import fr.itsonus.bousoleplussbackend.payload.request.LoginRequest;
import fr.itsonus.bousoleplussbackend.payload.request.RegisterRequest; import fr.itsonus.bousoleplussbackend.payload.request.SignupRequest;
import fr.itsonus.bousoleplussbackend.payload.request.TokenRefreshRequest; import fr.itsonus.bousoleplussbackend.payload.request.TokenRefreshRequest;
import fr.itsonus.bousoleplussbackend.payload.response.JwtResponse; import fr.itsonus.bousoleplussbackend.payload.response.JwtResponse;
import fr.itsonus.bousoleplussbackend.security.jwt.JwtGenerator; 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.services.RefreshTokenService; import fr.itsonus.bousoleplussbackend.security.services.RefreshTokenService;
import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl; import fr.itsonus.bousoleplussbackend.security.services.UserDetailsImpl;
import fr.itsonus.bousoleplussbackend.usecase.UserCreationUseCase;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.context.SecurityContextHolder; 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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
@AllArgsConstructor @AllArgsConstructor
public class AuthController { public class AuthController {
private final DaoAuthenticationProvider userAuthenticationProvider; private final AuthenticationManager authenticationManager;
private final AuthenticationService authenticationService;
private final UserCreationUseCase userCreationUseCase; private final UserRepository userRepository;
private final JwtGenerator jwtGenerator;
private final PasswordEncoder encoder;
private final JwtUtils jwtUtils;
private final RefreshTokenService refreshTokenService; private final RefreshTokenService refreshTokenService;
@PostMapping("/login") @PostMapping("/signin")
public JwtResponse login(@Valid @RequestBody LoginRequest loginRequest) { public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
var authentication = userAuthenticationProvider var authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())); .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
var userDetails = (UserDetailsImpl) authentication.getPrincipal(); var userDetails = (UserDetailsImpl) authentication.getPrincipal();
var token = jwtGenerator.generateAuthToken(authentication); var jwt = jwtUtils.generateJwtToken(userDetails);
var refreshToken = refreshTokenService.createOrUpdateRefreshToken(userDetails.getId()); var refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
return new JwtResponse(token, refreshToken.getToken()); return ResponseEntity.ok(new JwtResponse(jwt, refreshToken.getToken(), userDetails.getId(),
userDetails.getUsername()));
} }
@PostMapping("/register") @PostMapping("/signup")
@ResponseStatus(HttpStatus.CREATED) public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
public void registerUser(@Valid @RequestBody RegisterRequest request) { if (userRepository.existsByUsername(signUpRequest.getUsername())) {
userCreationUseCase.createUserAndDefaultBundle(request.toUser()); return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
}
// Create new user's account
var user = new User();
user.setUsername(signUpRequest.getUsername());
user.setPassword(encoder.encode(signUpRequest.getPassword()));
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
} }
@PostMapping("/refresh-token") @PostMapping("/refreshtoken")
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) { public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
var requestRefreshToken = request.getRefreshToken(); String requestRefreshToken = request.getRefreshToken();
return refreshTokenService.findByToken(requestRefreshToken) return refreshTokenService.findByToken(requestRefreshToken)
.map(refreshTokenService::verifyExpiration) .map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser) .map(RefreshToken::getUser)
.map(user -> { .map(user -> {
var token = jwtGenerator.generateRefreshToken(user.getEmail()); String token = jwtUtils.generateTokenFromUsername(user.getUsername());
return ResponseEntity.ok(token); return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
}) })
.orElseThrow(() -> new TokenRefreshException(requestRefreshToken, "Refresh token unknown")); .orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
"Refresh token is not in database!"));
} }
@PostMapping("/logout") @PostMapping("/logout")
public void logoutUser(@Valid @RequestBody LogOutRequest logOutRequest) { public ResponseEntity<?> logoutUser(@Valid @RequestBody LogOutRequest logOutRequest) {
refreshTokenService.deleteByUserId(logOutRequest.getUserId()); refreshTokenService.deleteByUserId(logOutRequest.getUserId());
return ResponseEntity.ok(new MessageResponse("Log out successful!"));
} }
@GetMapping("/me") @GetMapping("/me")
public User me() { public ResponseEntity<?> me() {
return authenticationService.getCurrentUser(); var authentication = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(authentication.getPrincipal());
} }
} }

View File

@ -1,22 +0,0 @@
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();
}
}

View File

@ -1,47 +0,0 @@
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);
}
}

View File

@ -1,31 +0,0 @@
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);
}
}

View File

@ -1,42 +1,58 @@
package fr.itsonus.bousoleplussbackend.controllers; package fr.itsonus.bousoleplussbackend.controllers;
import fr.itsonus.bousoleplussbackend.domain.quiz.model.Quiz; import fr.itsonus.bousoleplussbackend.models.Quiz;
import fr.itsonus.bousoleplussbackend.domain.quiz.model.QuizDetailed; import fr.itsonus.bousoleplussbackend.models.QuizScore;
import fr.itsonus.bousoleplussbackend.models.Response;
import fr.itsonus.bousoleplussbackend.payload.request.QuizRequest; import fr.itsonus.bousoleplussbackend.payload.request.QuizRequest;
import fr.itsonus.bousoleplussbackend.usecase.QuizUseCase; import fr.itsonus.bousoleplussbackend.repositories.QuestionRepository;
import jakarta.validation.Valid; import fr.itsonus.bousoleplussbackend.repositories.QuizRepository;
import fr.itsonus.bousoleplussbackend.repositories.QuizScoreRepository;
import fr.itsonus.bousoleplussbackend.repositories.ResponseRepository;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/quizzes") @RequestMapping("/quizzes")
@AllArgsConstructor @AllArgsConstructor
public class QuizController { public class QuizController {
private final QuizUseCase quizUseCase; private QuizRepository quizRepository;
private ResponseRepository responseRepository;
private QuestionRepository questionRepository;
private QuizScoreRepository quizScoreRepository;
@PostMapping @PostMapping("batch") // TODO add to REST representation
public Quiz create(@Valid @RequestBody QuizRequest request) { public Quiz create(@Valid @RequestBody QuizRequest request) {
return quizUseCase.create(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;
} }
@GetMapping("search") @GetMapping("{id}/scores")
public Page<QuizDetailed> findAllForCurrentUser(@RequestParam Long bundleId, Pageable pageable) { public Iterable<QuizScore> findScores(@PathVariable("id") Long id) {
return this.quizUseCase.findAllForCurrentUser(bundleId, pageable); return this.quizScoreRepository.findAllByQuizId(id);
}
@GetMapping("{id}")
public QuizDetailed findById(@PathVariable("id") Long id) {
return this.quizUseCase.findById(id);
} }
} }

View File

@ -1,23 +0,0 @@
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);
}
}

View File

@ -1,7 +0,0 @@
package fr.itsonus.bousoleplussbackend.domain.auth.exception;
public class AlreadyExistingUserException extends RuntimeException {
public AlreadyExistingUserException(String message) {
super(message);
}
}

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -1,22 +0,0 @@
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;
}

View File

@ -1,18 +0,0 @@
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);
}

View File

@ -1,15 +0,0 @@
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);
}

View File

@ -1,6 +0,0 @@
package fr.itsonus.bousoleplussbackend.domain.notification;
public interface NotificationService {
void notify(String from, String to, String subject, String text);
}

View File

@ -1,15 +0,0 @@
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;
}

View File

@ -1,17 +0,0 @@
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;
}

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -1,20 +0,0 @@
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;
}

View File

@ -1,12 +0,0 @@
package fr.itsonus.bousoleplussbackend.domain.quiz.model;
import lombok.Data;
import java.util.Date;
@Data
public class Quiz {
private Long id;
private Date createdDate;
}

View File

@ -1,34 +0,0 @@
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;
}
}

View File

@ -1,6 +0,0 @@
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) {
}

View File

@ -1,7 +0,0 @@
package fr.itsonus.bousoleplussbackend.domain.quiz.spi;
import fr.itsonus.bousoleplussbackend.domain.quiz.model.Axe;
public interface AxeCacheRepository {
Iterable<Axe> findAll();
}

View File

@ -1,16 +0,0 @@
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);
}

View File

@ -1,15 +0,0 @@
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);
}

View File

@ -1,19 +0,0 @@
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);
}

View File

@ -1,15 +0,0 @@
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);
}

View File

@ -1,7 +0,0 @@
package fr.itsonus.bousoleplussbackend.exception;
public class InvalidPasswordException extends RuntimeException {
public InvalidPasswordException(String message) {
super(message);
}
}

View File

@ -1,7 +0,0 @@
package fr.itsonus.bousoleplussbackend.exception;
public class NoCurrentUserException extends RuntimeException {
public NoCurrentUserException() {
super("No current user logged");
}
}

View File

@ -1,7 +1,13 @@
package fr.itsonus.bousoleplussbackend.exception; 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 { public class TokenRefreshException extends RuntimeException {
private static final long serialVersionUID = 1L;
public TokenRefreshException(String token, String message) { public TokenRefreshException(String token, String message) {
super(String.format("Failed for [%s]: %s", token, message)); super(String.format("Failed for [%s]: %s", token, message));
} }

View File

@ -1,8 +0,0 @@
package fr.itsonus.bousoleplussbackend.exception;
public class UnknownEmailException extends RuntimeException {
public UnknownEmailException() {
super("Cet e-mail est inconnu.");
}
}

View File

@ -1,118 +0,0 @@
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);
}
}

View File

@ -1,24 +0,0 @@
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);
}
}

View File

@ -1,20 +0,0 @@
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);
}

View File

@ -1,47 +0,0 @@
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));
}
}

View File

@ -1,16 +0,0 @@
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);
}

View File

@ -1,39 +0,0 @@
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);
}
}

View File

@ -1,71 +0,0 @@
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();
}
}

View File

@ -1,74 +0,0 @@
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();
}
}

View File

@ -1,66 +0,0 @@
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();
}
}

View File

@ -1,66 +0,0 @@
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();
}
}

View File

@ -1,87 +0,0 @@
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();
}
}

View File

@ -1,12 +0,0 @@
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();
}

View File

@ -1,20 +0,0 @@
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();
}
}

View File

@ -1,16 +0,0 @@
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);
}

View File

@ -1,58 +0,0 @@
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();
}
}

View File

@ -1,22 +0,0 @@
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);
}

View File

@ -1,46 +0,0 @@
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());
}
}

View File

@ -1,24 +0,0 @@
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);
}

View File

@ -1,41 +0,0 @@
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();
}
}

View File

@ -1,13 +0,0 @@
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);
}

View File

@ -1,43 +0,0 @@
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();
}
}

View File

@ -1,19 +1,18 @@
package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; package fr.itsonus.bousoleplussbackend.models;
import com.fasterxml.jackson.annotation.JsonIgnore; 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.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.Hibernate; 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.Objects;
import java.util.Set; import java.util.Set;
@ -21,8 +20,8 @@ import java.util.Set;
@Setter @Setter
@ToString @ToString
@RequiredArgsConstructor @RequiredArgsConstructor
@Entity(name = "axe") @Entity
public class PostgresAxeDto { public class Axe {
@Id @Id
@GeneratedValue @GeneratedValue
@ -47,24 +46,13 @@ public class PostgresAxeDto {
@OneToMany(mappedBy = "axe") @OneToMany(mappedBy = "axe")
@ToString.Exclude @ToString.Exclude
@JsonIgnore @JsonIgnore
private Set<PostgresQuestionDto> questions; private Set<Question> questions;
public Axe toAxe() {
return Axe.builder()
.id(id)
.identifier(identifier)
.shortTitle(shortTitle)
.title(title)
.description(description)
.color(color)
.build();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
PostgresAxeDto axe = (PostgresAxeDto) o; Axe axe = (Axe) o;
return id != null && Objects.equals(id, axe.id); return id != null && Objects.equals(id, axe.id);
} }

View File

@ -0,0 +1,66 @@
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();
}
}

View File

@ -0,0 +1,70 @@
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();
}
}

View File

@ -0,0 +1,36 @@
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;
}

View File

@ -0,0 +1,51 @@
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();
}
}

View File

@ -1,15 +1,6 @@
package fr.itsonus.bousoleplussbackend.infrastructure.postgres.models; package fr.itsonus.bousoleplussbackend.models;
import com.fasterxml.jackson.annotation.JsonIgnore; 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.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
@ -18,6 +9,14 @@ import lombok.experimental.Accessors;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.validator.constraints.Length; 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; import java.util.Objects;
@Getter @Getter
@ -25,8 +24,8 @@ import java.util.Objects;
@ToString @ToString
@RequiredArgsConstructor @RequiredArgsConstructor
@Accessors(chain = true) @Accessors(chain = true)
@Entity(name = "response") @Entity
public class PostgresQuizResponseDto { public class Response {
@Id @Id
@GeneratedValue @GeneratedValue
@ -34,12 +33,12 @@ public class PostgresQuizResponseDto {
@OneToOne @OneToOne
@JoinColumn(name = "question_id") @JoinColumn(name = "question_id")
private PostgresQuestionDto question; private Question question;
@ManyToOne @ManyToOne
@JoinColumn(name = "quiz_id") @JoinColumn(name = "quiz_id")
@JsonIgnore @JsonIgnore
private PostgresQuizDto quiz; private Quiz quiz;
@Min(0) @Min(0)
@Max(10) @Max(10)
@ -48,19 +47,11 @@ public class PostgresQuizResponseDto {
@Length(max = 500) @Length(max = 500)
private String comment; private String comment;
public QuizResponse toQuizResponse() {
return new QuizResponse(
getQuestion().getAxe().getIdentifier(),
getQuestion().getLabel(),
getScore(),
getComment());
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
PostgresQuizResponseDto response = (PostgresQuizResponseDto) o; Response response = (Response) o;
return id != null && Objects.equals(id, response.id); return id != null && Objects.equals(id, response.id);
} }

View File

@ -0,0 +1,54 @@
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();
}
}

View File

@ -1,23 +0,0 @@
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();
}
}
}

View File

@ -1,14 +1,13 @@
package fr.itsonus.bousoleplussbackend.payload.request; package fr.itsonus.bousoleplussbackend.payload.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data @Data
public class LoginRequest { public class LoginRequest {
@NotBlank @NotBlank
@Email private String username;
private String email;
@NotBlank @NotBlank
private String password; private String password;

View File

@ -1,6 +0,0 @@
package fr.itsonus.bousoleplussbackend.payload.request;
import jakarta.validation.constraints.Email;
public record NotifyPasswordResetRequest(@Email String email) {
}

View File

@ -1,12 +1,14 @@
package fr.itsonus.bousoleplussbackend.payload.request; package fr.itsonus.bousoleplussbackend.payload.request;
import jakarta.validation.Valid; import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.Set; import java.util.Set;
public record QuizRequest(@NotNull Long bundleId, @Data
@NotEmpty Set<@Valid ResponseRequest> responses) { public class QuizRequest {
@NotEmpty
private Set<@Valid ResponseRequest> responses;
} }

View File

@ -1,32 +0,0 @@
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();
}
}

View File

@ -1,10 +0,0 @@
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) {
}

View File

@ -1,8 +1,9 @@
package fr.itsonus.bousoleplussbackend.payload.request; package fr.itsonus.bousoleplussbackend.payload.request;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotNull;
@Data @Data
public class ResponseRequest { public class ResponseRequest {
@NotNull @NotNull

View File

@ -0,0 +1,17 @@
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;
}

View File

@ -1,8 +1,9 @@
package fr.itsonus.bousoleplussbackend.payload.request; package fr.itsonus.bousoleplussbackend.payload.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data @Data
public class TokenRefreshRequest { public class TokenRefreshRequest {
@NotBlank @NotBlank

View File

@ -1,8 +0,0 @@
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) {
}

View File

@ -1,9 +0,0 @@
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) {
}

View File

@ -1,26 +0,0 @@
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) {
}
}

View File

@ -1,17 +1,15 @@
package fr.itsonus.bousoleplussbackend.payload.response; package fr.itsonus.bousoleplussbackend.payload.response;
import fr.itsonus.bousoleplussbackend.infrastructure.postgres.models.PostgresQuestionDto; import fr.itsonus.bousoleplussbackend.models.Question;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.RepresentationModel;
@Data @Data
@EqualsAndHashCode(callSuper = true)
public class AxeWithQuestion extends RepresentationModel<AxeWithQuestion> { public class AxeWithQuestion extends RepresentationModel<AxeWithQuestion> {
private Integer identifier; private Integer identifier;
private String shortTitle; private String shortTitle;
private String title; private String title;
private String color; private String color;
private Iterable<PostgresQuestionDto> questions; private Iterable<Question> questions;
} }

View File

@ -1,12 +1,15 @@
package fr.itsonus.bousoleplussbackend.payload.response; package fr.itsonus.bousoleplussbackend.payload.response;
import fr.itsonus.bousoleplussbackend.security.jwt.Token;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public class JwtResponse { public class JwtResponse {
private Token token;
private static String type = "Bearer";
private String token;
private String refreshToken; private String refreshToken;
private Long id;
private String username;
} }

View File

@ -7,7 +7,7 @@ import lombok.Data;
@AllArgsConstructor @AllArgsConstructor
public class TokenRefreshResponse { public class TokenRefreshResponse {
private String tokenType; private static String tokenType = "Bearer";
private String accessToken; private String accessToken;
private String refreshToken; private String refreshToken;
} }

View File

@ -1,20 +0,0 @@
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é dau 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 {};
}

View File

@ -1,12 +0,0 @@
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,}$");
}
}

View File

@ -0,0 +1,11 @@
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();
}

View File

@ -0,0 +1,21 @@
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();
}

View File

@ -0,0 +1,20 @@
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();
}

View File

@ -0,0 +1,18 @@
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();
}

View File

@ -0,0 +1,11 @@
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> {
}

View File

@ -0,0 +1,17 @@
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);
}

View File

@ -0,0 +1,18 @@
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);
}

View File

@ -0,0 +1,11 @@
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);
}

View File

@ -0,0 +1,17 @@
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);
}

View File

@ -0,0 +1,11 @@
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> {
}

View File

@ -0,0 +1,14 @@
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);
}

View File

@ -1,49 +0,0 @@
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();
}
}

View File

@ -1,67 +0,0 @@
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;
}
}

View File

@ -1,20 +0,0 @@
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");
}
}

View File

@ -0,0 +1,45 @@
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");
}
}

View File

@ -0,0 +1,57 @@
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;
}
}

View File

@ -1,25 +0,0 @@
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());
}
}
}

View File

@ -1,50 +0,0 @@
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