Documentation Complète - QuizMaker
- 1. PARTIE 1 : INTRODUCTION ET ARCHITECTURE
- 2. 1. Vue d’Ensemble du Projet
- 3. 2. Architecture Globale
- 4. PARTIE 2 : IMPLÉMENTATION BACKEND
- 5. 3. Structure du Backend
- 6. 4. Modèle de Domaine
- 7. 5. Sécurité et Authentification JWT
- 8. PARTIE 3 : IMPLÉMENTATION FRONTEND
- 9. 6. Structure Angular
- 10. PARTIE 4 : DÉPLOIEMENT ET TESTS
- 11. 7. Docker et Conteneurisation
- 12. 8. Tests et Qualité
- 13. PARTIE 5 : DOCUMENTATION API ET UTILISATION
- 14. 9. API Endpoints
- 15. 10. Guide d’Utilisation
- 16. 11. Conformité aux Exigences
- 17. 12. Points Forts du Projet
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 :
-
quizmaker-db : PostgreSQL 15
-
quizmaker-backend : API Spring Boot (port 8080)
-
quizmaker-frontend : Application Angular + Nginx (port 4200)
-
pgAdmin : Interface de gestion PostgreSQL (port 5050)
-
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) │
└─────────────────────────────────────────┘
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
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();
}
}
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;
}
}
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);
});
});
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"
}'
15. 10. Guide d’Utilisation
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
-
Frontend: http://localhost:4200
-
Backend API: http://localhost:8080/api
-
Swagger UI: http://localhost:8080/swagger-ui.html
-
pgAdmin: http://localhost:5050
-
Adminer: http://localhost:8081
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
-
Architecture Clean: Séparation claire des responsabilités
-
Sécurité Robuste: JWT + Spring Security + BCrypt
-
Multi-Environnements: Profils Spring (dev/docker/prod)
-
Tests Complets: Unitaires + Intégration
-
CI/CD Ready: Dockerisation complète
-
API Documentée: OpenAPI/Swagger
-
Code Quality: Conventions Google, Lombok, Logs
-
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