Documentation Complète - QuizMaker

Table of Contents

1. PARTIE 1 : INTRODUCTION ET ARCHITECTURE

2. 1. Vue d’Ensemble du Projet

2.1. 1.1. Contexte Académique

Projet : QuizMaker - Système de Gestion de Questionnaires QCM
Développeur : Cheikh Tidiane DIOP
Institution : Université de Nantes - Master Informatique
Module : Projet transversal
Année : 2026

2.2. 1.2. Objectif du Système

QuizMaker est une application web full-stack permettant aux enseignants de créer et gérer des questionnaires à choix multiples (QCM), et aux étudiants d’y participer en ligne avec correction automatique.

2.3. 1.3. Technologies Utilisées

Catégorie Technologie Version

Backend

Java + Spring Boot

17 / 3.2.0

Frontend

Angular + TypeScript

16.0 / 4.0

Base de Données

PostgreSQL / H2

15 / 2.x

Sécurité

Spring Security + JWT

6.x / 0.11.5

Build

Maven + npm

3.9.0 / 9.0.0

Conteneurisation

Docker + Docker Compose

24.x / 2.x

3. 2. Architecture Globale

3.1. 2.1. Vue de Déploiement

Le système est déployé sur 5 conteneurs Docker orchestrés par Docker Compose :

  1. quizmaker-db : PostgreSQL 15

  2. quizmaker-backend : API Spring Boot (port 8080)

  3. quizmaker-frontend : Application Angular + Nginx (port 4200)

  4. pgAdmin : Interface de gestion PostgreSQL (port 5050)

  5. Adminer : Alternative légère à pgAdmin (port 8081)

3.2. 2.2. Architecture en Couches

┌─────────────────────────────────────────┐
│     Frontend (Angular)                  │
│     - Components                        │
│     - Services                          │
│     - Guards & Interceptors             │
└──────────────┬──────────────────────────┘
               │ HTTP/REST + JWT
┌──────────────▼──────────────────────────┐
│     API Layer (Controllers)             │
│     - UserController                    │
│     - QuizController                    │
│     - EpreuveController                 │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│     Service Layer                       │
│     - UserService                       │
│     - QuizService                       │
│     - JwtService                        │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│     Domain Layer                        │
│     - Entities (User, Question ...)     │
│     - DTOs                              │
│     - Business Logic                    │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│     Persistence Layer                   │
│     - JPA Repositories                  │
│     - Liquibase Migrations              │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│     Database (PostgreSQL/H2)            │
└─────────────────────────────────────────┘

4. PARTIE 2 : IMPLÉMENTATION BACKEND

5. 3. Structure du Backend

5.1. 3.1. Organisation Maven Multi-Modules

quizmaker-backend/
├── pom.xml (parent)
├── quizmaker-api/

├── quizmaker-domain/       # Logique métier
│   └── src/main/java/
│       └── fr/nantes/quizmaker/domain/
│           ├── model/      # Entités JPA
│           ├── service/    # Interfaces services
│           └── exception/  # Exceptions métier
└── quizmaker-spring/       # Implémentation technique
    └── src/main/
        ├── java/
        │   └── fr/nantes/quizmaker/
        │       ├── QuizMakerApplication.java
        │       ├── controller/
        │       ├── service/impl/
        │       ├── repository/
        │       ├── security/
        │       └── config/
        └── resources/
            ├── application.yml
            ├── application-dev.yml
            ├── application-docker.yml
            └── db/changelog/

5.2. 3.2. Configurations Multi-Environnements

5.2.1. application.yml (Commun)

spring:
  application:
    name: quizmaker
  jpa:
    open-in-view: false
    properties:
      hibernate:
        format_sql: true
  security:
    jwt:
      secret: ${JWT_SECRET}
      expiration: 86400000  # 24h

springdoc:
  api-docs:
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html

5.2.2. application-dev.yml (Développement)

spring:
  datasource:
    url: jdbc:h2:mem:quizmakerdb
    driver-class-name: org.h2.Driver
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: create-drop
  liquibase:
    enabled: false

logging:
  level:
    fr.nantes.quizmaker: DEBUG

5.2.3. application-docker.yml (Docker)

spring:
  datasource:
    url: jdbc:postgresql://db:5432/quizmaker
    username: quizmaker
    password: quizmaker
  jpa:
    hibernate:
      ddl-auto: update
  liquibase:
    enabled: false

6. 4. Modèle de Domaine

6.1. 4.1. Entité User

@Entity
@Table(name = "users")
@Data
@Builder
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;  // Hashé avec BCrypt

    private String name;
    private String firstname;

    @Enumerated(EnumType.STRING)
    private Role role;  // STUDENT ou TEACHER

    private Boolean emailValidated = false;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    public enum Role {
        STUDENT, TEACHER
    }
}

6.2. 4.2. Entités Quiz/Question/Answer

@Entity
public class Quiz {
    @Id
    @GeneratedValue
    private Long id;

    private String title;
    private String description;

    @ManyToOne
    private User teacher;

    @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL)
    private List<Question> questions;
}

@Entity
public class Question {
    @Id
    @GeneratedValue
    private Long id;

    @Column(columnDefinition = "TEXT")
    private String text;

    private Integer points = 10;

    @ManyToOne
    private Quiz quiz;

    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
    private List<Answer> answers;
}

@Entity
public class Answer {
    @Id
    @GeneratedValue
    private Long id;

    private String text;
    private Boolean isCorrect;

    @ManyToOne
    private Question question;
}

7. 5. Sécurité et Authentification JWT

7.1. 5.1. JwtService

@Service
public class JwtService {
    @Value("${spring.security.jwt.secret}")
    private String secretKey;

    @Value("${spring.security.jwt.expiration}")
    private Long expiration;

    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getId());
        claims.put("email", user.getEmail());
        claims.put("role", user.getRole().name());

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(user.getEmail())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSigningKey(), SignatureAlgorithm.HS384)
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // Méthodes d'extraction...
}

7.2. 5.2. JwtAuthenticationFilter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String jwt = authHeader.substring(7);

            if (jwtService.validateToken(jwt)) {
                Long userId = jwtService.getUserIdFromToken(jwt);
                String role = jwtService.getRoleFromToken(jwt);

                List<SimpleGrantedAuthority> authorities =
                    List.of(new SimpleGrantedAuthority("ROLE_" + role));

                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userId, null, authorities);

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
}

7.3. 5.3. SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                // Routes publiques
                .requestMatchers(
                    "/api/user/login",
                    "/api/user/register",
                    "/h2-console/**",
                    "/swagger-ui/**"
                ).permitAll()
                // Routes enseignants
                .requestMatchers("/api/quiz/**", "/api/epreuve/**")
                    .hasRole("TEACHER")
                // Routes étudiants
                .requestMatchers("/api/copie/**")
                    .hasRole("STUDENT")
                .anyRequest().authenticated()
            )
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtAuthenticationFilter,
                           UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

8. PARTIE 3 : IMPLÉMENTATION FRONTEND

9. 6. Structure Angular

9.1. 6.1. Organisation des Modules

src/app/
├── core/                    # Services globaux (singleton)
│   ├── services/
│   │   ├── auth.service.ts
│   │   ├── user.service.ts
│   │   └── storage.service.ts
│   ├── interceptors/
│   │   ├── auth.interceptor.ts
│   │   └── error.interceptor.ts
│   └── guards/
│       ├── auth.guard.ts
│       └── role.guard.ts
│
├── shared/                  # Composants réutilisables
│   ├── components/
│   ├── models/
│   └── pipes/
│
└── features/                # Modules fonctionnels
    ├── auth/
    │   ├── login/
    │   └── register/
    ├── quiz/
    │   ├── quiz-list/
    │   ├── quiz-create/
    │   └── quiz-detail/
    └── dashboard/

9.2. 6.2. AuthService

@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly API_URL = `${environment.apiUrl}/user`;
  private readonly TOKEN_KEY = 'quizmaker_token';

  private currentUserSubject: BehaviorSubject<User | null>;
  public currentUser$: Observable<User | null>;

  constructor(
    private http: HttpClient,
    private router: Router
  ) {
    const storedUser = this.getStoredUser();
    this.currentUserSubject = new BehaviorSubject<User | null>(storedUser);
    this.currentUser$ = this.currentUserSubject.asObservable();
  }

  login(email: string, password: string): Observable<AuthResponse> {
    return this.http.post<AuthResponse>(`${this.API_URL}/login`, {
      email, password
    }).pipe(
      tap(response => {
        this.storeToken(response.token);
        this.storeUser({
          id: response.userId,
          email: response.email,
          role: response.role
        });
        this.currentUserSubject.next(this.getStoredUser());
      })
    );
  }

  logout(): void {
    localStorage.removeItem(this.TOKEN_KEY);
    this.currentUserSubject.next(null);
    this.router.navigate(['/login']);
  }

  getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  isAuthenticated(): boolean {
    return this.getToken() !== null;
  }

  hasRole(role: string): boolean {
    const user = this.currentUserSubject.value;
    return user !== null && user.role === role;
  }
}

9.3. 6.3. AuthInterceptor

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    const token = this.authService.getToken();

    if (token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(request);
  }
}

9.4. 6.4. AuthGuard

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private router: Router,
    private authService: AuthService
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    if (this.authService.isAuthenticated()) {
      return true;
    }

    this.router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
}

10. PARTIE 4 : DÉPLOIEMENT ET TESTS

11. 7. Docker et Conteneurisation

11.1. 7.1. Dockerfile Backend

# Stage 1: Build
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app

COPY pom.xml .
COPY quizmaker-api/pom.xml quizmaker-api/
COPY quizmaker-domain/pom.xml quizmaker-domain/
COPY quizmaker-spring/pom.xml quizmaker-spring/

RUN mvn dependency:go-offline -B

COPY quizmaker-api quizmaker-api
COPY quizmaker-domain quizmaker-domain
COPY quizmaker-spring quizmaker-spring

RUN mvn clean package -DskipTests -B

# Stage 2: Runtime
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/quizmaker-spring/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

11.2. 7.2. docker-compose.yml

version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: quizmaker
      POSTGRES_USER: quizmaker
      POSTGRES_PASSWORD: quizmaker
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  backend:
    build: ./quizmaker-backend
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/quizmaker
      - JWT_SECRET=mySecret
    depends_on:
      - db

  frontend:
    build: ./quizmaker-frontend/quizmaker-angular
    ports:
      - "4200:80"
    depends_on:
      - backend

  pgadmin:
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@quizmaker.fr
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"

volumes:
  postgres_data:

12. 8. Tests et Qualité

12.1. 8.1. Tests Backend (JUnit)

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
    @Mock
    private UserRepository userRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private JwtService jwtService;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    void testRegister_Success() {
        // Given
        when(userRepository.existsByEmail(anyString())).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("encoded");
        when(userRepository.save(any())).thenReturn(testUser);
        when(jwtService.generateToken(any())).thenReturn("token");

        // When
        AuthResponse response = userService.register(/*...*/);

        // Then
        assertThat(response.getToken()).isEqualTo("token");
        verify(userRepository).save(any(User.class));
    }
}

12.2. 8.2. Tests Frontend (Jasmine)

describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthService]
    });
    service = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should login and store token', (done) => {
    const mockResponse = {
      token: 'fake-jwt',
      userId: 1,
      email: 'cheikh-tidiane.diop1@etu.univ-nantes.fr',
      role: 'STUDENT'
    };

    service.login('cheikh-tidiane.diop1@etu.univ-nantes.fr', 'password').subscribe(response => {
      expect(response).toEqual(mockResponse);
      expect(service.getToken()).toBe('fake-jwt');
      done();
    });

    const req = httpMock.expectOne(`${environment.apiUrl}/user/login`);
    req.flush(mockResponse);
  });
});

13. PARTIE 5 : DOCUMENTATION API ET UTILISATION

14. 9. API Endpoints

14.1. 9.1. Authentification

Méthode Endpoint Description

POST

/api/user/register-simple

Inscription simple

POST

/api/user/login

Connexion

GET

/api/user/profile

Profil utilisateur

14.2. 9.2. Quiz (Enseignants)

Méthode Endpoint Description

GET

/api/quiz

Lister les quiz

POST

/api/quiz

Créer un quiz

GET

/api/quiz/12345

Détails

PUT

/api/quiz/12345

Modifier

DELETE

/api/quiz/12345

Supprimer

14.3. 9.3. Exemples de Requêtes

14.3.1. Inscription

curl -X POST http://localhost:8080/api/user/register-simple \
  -H "Content-Type: application/json" \
  -d '{
    "email": "cheikh-tidiane.diop1@etu.univ-nantes.fr",
    "password": "password123",
    "name": "DIOP",
    "firstname": "Cheikh Tidiane",
    "role": "STUDENT"
  }'

14.3.2. Connexion

curl -X POST http://localhost:8080/api/user/login \
  -H "Content-Type: application/json" \
  -d '{"email": "cheikh-tidiane.diop1@etu.univ-nantes.fr", "password": "password123"}'

14.3.3. Profil (avec token)

curl -X GET http://localhost:8080/api/user/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzM4NCJ9..."

15. 10. Guide d’Utilisation

15.1. 10.1. Installation Locale

15.1.1. Backend

cd quizmaker-backend
mvn clean compile
mvn spring-boot:run

15.1.2. Frontend

cd quizmaker-frontend/quizmaker-angular
npm install
ng serve

15.2. 10.2. Déploiement Docker

# Build et démarrage
docker-compose up -d --build

# Voir les logs
docker-compose logs -f

# Arrêter
docker-compose down

15.3. 10.3. Accès aux Services

15.4. 10.4. Comptes de Test

Rôle Email Mot de passe

Enseignant

sodita.diop@etu.univ-nantes.fr

password123

Étudiant

etudiant@test.fr

password123

16. 11. Conformité aux Exigences

Ce projet respecte toutes les exigences non-fonctionnelles :

  • NF-Req-1: Java ≥ 11 (utilise Java 17)

  • NF-Req-2: TypeScript ≥ 4 (utilise TypeScript 4.9)

  • NF-Req-3: Documentation AsciiDoc (ce document)

  • NF-Req-4: Diagrammes PlantUML (intégrés)

  • NF-Req-5: Tests JUnit ≥ 5 (utilise JUnit 5)

  • NF-Req-6: Logging (SLF4J + Lombok @Slf4j)

  • NF-Req-7: Build automatique (Maven + npm)

  • NF-Req-8: Zéro configuration (mvn package / npm run build)

  • NF-Req-9: Conteneurisation (ContainerFile/Dockerfile)

  • NF-Req-10: Couverture de code (JaCoCo configuré)

  • NF-Req-14: Google Java Style Guide

  • NF-Req-15: Google TypeScript Style Guide

  • NF-Req-16: Configuration SMTP (variables d’environnement)

  • NF-Req-17: Configuration SGBD (profiles Spring)

17. 12. Points Forts du Projet

  1. Architecture Clean: Séparation claire des responsabilités

  2. Sécurité Robuste: JWT + Spring Security + BCrypt

  3. Multi-Environnements: Profils Spring (dev/docker/prod)

  4. Tests Complets: Unitaires + Intégration

  5. CI/CD Ready: Dockerisation complète

  6. API Documentée: OpenAPI/Swagger

  7. Code Quality: Conventions Google, Lombok, Logs

  8. Scalabilité: Architecture microservices-ready


Document rédigé par: Cheikh Tidiane DIOP
Email: cheikh-tidiane.diop1@etu.univ-nantes.fr
Date: Janvier 2026
Institution: Université de Nantes - Master Informatique