Configuration du module Spring

1. Structure

📁 quizmaker-backend/
 └── 📁 quizmaker-spring/
     ├── src/main/java/fr/nantes/universite/quizmaker/
     │   ├── controller/        # Contrôleurs (points d'entrée de l'API REST)
     │   ├── service/           # Implémentations  des services
     │   └── QuizmakerApplication.java  # Point d'entrée Spring Boot
     └── src/main/resources/
         ├── db/changelog/      # Scripts Liquibase de migration de BDD
         ├── application.yml     # Configuration centrale (BDD, Liquibase, Swagger, etc.)
         └── openapi/
             └── openapi.yaml    # Spécification OpenAPI des routes à générer

2. Configuration des paquets Java

Pour commencer, vous allez créer trois paquets Java qui vous aideront à organiser vos classes .

  1. Créez les paquet suivants à l’intérieur du dossier des sources du module quizmaker-spring :

    • fr.nantes.universite.quizmaker.data.controller

    • fr.nantes.universite.quizmaker.data.service

  2. Profitez pour créer un dossier pour les ressources du module :

mkdir -p ./src/main/resources

3. L’application

Pour lancer une application Spring, vous aurez besoin d’une classe principale, qui doit être annotée comme une application Spring Boot : @SpringBootApplication.

  1. Ajoutez une classe nommée QuizmakerApplication au paquet fr.nantes.universite.quizmaker.spring.

  2. Le code source de cette classe doit ressembler à :

    @SpringBootApplication(scanBasePackages = "fr.nantes.universite.quizmaker")
    public class QuizmakerApplication {
      public static void main(String[] args) {
        SpringApplication.run(QuizmakerApplication.class, args);
      }
    }
  3. En passant votre classe (et non une instance) à SpringApplication::run(), vous permettez à cette méthode de la parser et lire ses annotations.
    L’attribut de l’annotation, scanBasePackages, dit à Spring où chercher les contrôleurs, les beans, les services, etc.

4. Les contrôleurs

Le paquet fr.nantes.universite.quizmaker.data.controller contient les contrôleurs REST, responsables de l’exposition des endpoints de l’API.

Dans le cadre de ce projet, les classes présentes ici implémentent les interfaces générées automatiquement par OpenAPI à partir du fichier openapi.yaml. Chaque contrôleur correspond à un domaine fonctionnel (p.ex. : AuthApiController, UserApiController, etc.).

Les contrôleurs ne doivent contenir aucune logique métier. Ils se contentent de :

  1. recevoir les requêtes HTTP,

  2. extraire les données nécessaires,

  3. déléguer le traitement à un service via son interface,

  4. retourner la réponse au client (souvent un DTO).

Exemple simplifié d’un contrôleur
public class AuthApiController implements AuthApi {
    private final AuthService authService;

    public AuthApiController(AuthService authService) {
        this.authService = authService;
    }

    @Override
    public ResponseEntity<AuthResponse> login(LoginRequest request) {
        return ResponseEntity.ok(authService.login(request));
    }
}

5. Les services

Le paquet fr.nantes.universite.quizmaker.data.service contient les classes qui implémentent les services déclarés dans le module API.

C’est ici que réside toute la logique métier : vérifications, appels aux référentiels, traitement de données, règles métier, etc. Chaque classe dans service/ implémente une interface définie dans le module API.

La logique métier ne doit jamais être placée dans les contrôleurs ou les repositories. Elle doit toujours être centralisée ici, dans les services métier.

Exemple de service
public class AuthServiceImpl implements AuthService {

    private final UserRepository userRepository;

    public AuthServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public AuthResponse login(LoginRequest request) {
        User user = userRepository.findByEmail(request.email())
            .orElseThrow(() -> new UnauthorizedException("Utilisateur non trouvé"));

        // Exemple simple : vérification brute du mot de passe (à sécuriser)
        if (!user.getPassword().equals(request.password())) {
            throw new UnauthorizedException("Mot de passe invalide");
        }

        return new AuthResponse("token-exemple");
    }
}

Le fichier openapi.yaml est situé dans quizmaker-spring/src/main/resources/openapi/ afin de centraliser les spécifications de l’API REST. Ce fichier est utilisé pour générer automatiquement les contrôleurs et les modèles, ou comme documentation interactive via Swagger.

6. Implémentation d’un service

Vous allez à présent coder la classe UserServiceImpl, qui implémente le service UserService.

  1. Créez la classe UserServiceImpl et implémentez la méthode login().

  2. Cette méthode doit :

    1. vérifier si l’adresse email et le mot de passe correspondent à un étudiant ou un enseignant ;

    2. renvoyer le DTO User avec le bon rôle (STUDENT ou TEACHER) ;

    3. lever une exception si les identifiants sont invalides.

Indication de code pour UserServiceImpl
 public User login(String email, String password) {
        PersistentStudent s = studentRepository.findAll().stream()
                .filter(u -> email != null && u.getEmail() != null && u.getEmail().equalsIgnoreCase(email))
                .filter(u -> password != null && u.getPassword() != null && u.getPassword().equals(password))
                .findFirst()
                .orElse(null);

        if (s != null) {
            return new User(
                    s.getId(),
                    s.getEmail(),
                    s.getName(),
                    s.getFirstName(),
                    ROLE_STUDENT
            );
        }

        PersistentTeacher t = teacherRepository.findAll().stream()
                .filter(u -> email != null && u.getEmail() != null && u.getEmail().equalsIgnoreCase(email))
                .filter(u -> password != null && u.getPassword() != null && u.getPassword().equals(password))
                .findFirst()
                .orElse(null);

        if (t != null) {
            return new User(
                    t.getId(),
                    t.getEmail(),
                    t.getName(),
                    t.getFirstName(),
                    ROLE_TEACHER
            );
        }

        throw new IllegalArgumentException("Invalid email or password.");
    }

7. Configuration d’OpenAPI

OpenAPI est une norme qui permet la spécification d’interfaces REST en YAML, pour ensuite générer le code, côté serveur et côté client, en différents langages de programmation. Voici un exemple de configuration OpenAPI pour le projet QuizMaker :

Exemple de configuration OpenAPI
Le fichier openapi.yaml
openapi: 3.0.1
info:
  title: QuizMaker API
  version: 1.0.0

paths:
  /api/user/login:
    post:
      tags:
        - User
      summary: Login
      description: Authentifie un utilisateur (étudiant ou enseignant) par email et mot de passe.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Utilisateur authentifié
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Requête invalide
        '401':
          description: Identifiants invalides

components:
  schemas:
    LoginRequest:
      type: object
      properties:
        email:
          type: string
          format: email
        password:
          type: string
      required: [email, password]

    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        name:
          type: string
        firstname:
          type: string
        role:
          type: string
          description: "STUDENT ou TEACHER"
      required: [id, email, name, firstname, role]
  1. À l’intérieur du dossier resources créé auparavant, créez un fichier nommé openapi.yaml, qui servira à définir les endpoints de notre interface.

    mkdir -p ./src/main/resources/openapi
    touch ./src/main/resources/openapi/openapi.yaml
  2. Copiez ensuite le contenu de l’exemple présenté ci-dessous dans ce nouveau fichier.

Lisez attentivement la configuration que vous venez de créer et identifiez les éléments suivantes :

  1. Un objet (ou enregistrement) nommé LoginRequest, contenant 2 propriétés ;

  2. Un objet nommé User, contenant 5 propriétés ;

  3. La route /api/user/login décrivant :

    1. qu’elle accepte une requête POST avec un LoginRequest contenant email et password.

    2. les réponses possibles (200 si succès, 401 si identifiants invalides).

    3. la réponse est relié à l’objet User.

  1. Utilisez Maven pour compiler la configuration OpenAPI

mvn compile
Explications

Lors de la compilation du projet, le plugin OpenAPI Generator :

  • lit ce fichier YAML,

  • génère automatiquement les classes Java correspondantes (interface UserApi, modèles User et LoginRequest),

  • place ces fichiers dans le dossier target/generated-sources/openapi/.

8. Lier le contrôleur au service

Pour lier l’interface REST à vous services, vous devez implémenter l’interface UserApi, qui a été générée automatiquement.

  1. Créez une classe nommée UserController, qui implémente la méthode apiUserLoginPost()

  2. Cette méthode :

    1. reçoit la requête HTTP,

    2. extrait les informations du LoginRequest,

    3. appelle userService.login(email, password),

    4. renvoie la réponse (User) au client.

Squelette de la classe UserController
@Controller
public class UserController implements UserApi {
    private final UserService userService;
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @Override
    public ResponseEntity<User> apiUserLoginPost(LoginRequest loginRequest) {
        //TODO: complete this function.
        return null; //TODO: remove this return.
    }
}
Indication de code pour UserController
public ResponseEntity<User> apiUserLoginPost(LoginRequest loginRequest) {
        try {
            var user = userService.login(loginRequest.getEmail(), loginRequest.getPassword());

            User response = new User()
                    .id(user.id())
                    .email(user.email())
                    .name(user.name())
                    .firstname(user.firstname())
                    .role(user.role());

            return ResponseEntity.ok(response);

        } catch (IllegalArgumentException e) {
            return ResponseEntity.status(401).build();
        } catch (Exception e) {
            return ResponseEntity.badRequest().build();
        }
    }

9. Configuration de la base de données avec Liquibase

Liquibase est une bibliothèque open source pour le suivi, la gestion et l’application des changements de schéma de base de données, indépendante du SGBD utilisé.

Dans notre projet, nous allons l’utiliser pour créer et versionner les tables de la base de données.

Exemple de configuration Liquibase
Le fichier master.xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
          http://www.liquibase.org/xml/ns/dbchangelog
          http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">

    <!-- Séquence utilisée par JPA (@GeneratedValue(strategy=SEQUENCE)) -->
    <changeSet id="1-seq" author="quiz-maker-author">
        <createSequence sequenceName="hibernate_sequence"/>
    </changeSet>

    <changeSet id="2-student" author="quiz-maker-author">
        <createTable tableName="student">
            <column name="id" type="BIGINT">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="email" type="VARCHAR(255)"/>
            <column name="password" type="VARCHAR(255)"/>
            <column name="name" type="VARCHAR(255)"/>
            <column name="firstname" type="VARCHAR(255)"/>
        </createTable>

        <addUniqueConstraint tableName="student" columnNames="email" constraintName="uk_student_email"/>
        <createIndex tableName="student" indexName="idx_student_email">
            <column name="email"/>
        </createIndex>
    </changeSet>

    <changeSet id="3-teacher" author="quiz-maker-author">
        <createTable tableName="teacher">
            <column name="id" type="BIGINT">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="email" type="VARCHAR(255)"/>
            <column name="password" type="VARCHAR(255)"/>
            <column name="name" type="VARCHAR(255)"/>
            <column name="firstname" type="VARCHAR(255)"/>
        </createTable>

        <addUniqueConstraint tableName="teacher" columnNames="email" constraintName="uk_teacher_email"/>
        <createIndex tableName="teacher" indexName="idx_teacher_email">
            <column name="email"/>
        </createIndex>
    </changeSet>

</databaseChangeLog>
  1. Créez le fichier de configuration Liquibase et copiez le contenu de l’exemple ci-dessus :

    mkdir -p ./src/main/resources/db/changelog/
    touch ./src/main/resources/db/changelog/master.xml

Lisez attentivement le fichier de configuration master.xml. Il définit les changements à appliquer (appelés changeSet).

Par exemple
  • création de la table student avec ses colonnes,

  • création de la table teacher.

Lors du démarrage de Spring Boot :

  • Liquibase détecte le changelog,

  • applique les modifications manquantes,

  • et maintient un historique des versions dans une table interne (DATABASECHANGELOG).

10. Conclusion

Voici l’enchaînement d’une requête de connexion :

  1. Le client envoie une requête POST vers /api/user/login avec l’email et le mot de passe.

  2. Spring Boot redirige cette requête vers la méthode correspondante du contrôleur généré via OpenAPI.

  3. Le contrôleur appelle le service UserService.

  4. Le service vérifie dans les repositories si un étudiant ou un enseignant correspond à l’email et au mot de passe.

  5. Si un utilisateur est trouvé :

    • un User DTO est créé et renvoyé au contrôleur,

    • qui le renvoie en réponse JSON au client avec un code HTTP 200.

  6. Sinon, une exception est levée, et la réponse HTTP est 401 Unauthorized.

Chaque couche a donc une responsabilité claire :

  • OpenAPI → définit la structure de l’API.

  • Contrôleur → reçoit et renvoie les données.

  • Service → applique la logique métier.

  • Repository → interagit avec la base.

  • Liquibase → gère la structure de la base.

11. Étendre le projet

Une fois la route /api/user/login fonctionnelle, il est possible d’y ajouter d’autres fonctionnalités :

  • créer de nouvelles routes OpenAPI (/api/user/register, /api/quiz/create, etc.),

  • ajouter de nouveaux changeSet Liquibase pour les tables nécessaires,

  • créer les DTO correspondants dans quizmaker-api,

  • implémenter les nouveaux services dans quizmaker-spring.

Cette approche garantit que chaque ajout reste cohérent et documenté, sans duplication ni conflit entre modules.