Directives de Code
Ce document définit les standards et les bonnes pratiques à suivre lors du développement et de la contribution au projet TrioSigno.
Principes Généraux
Nous suivons ces principes fondamentaux :
- Lisibilité - Le code doit être facile à lire et à comprendre
- Maintenabilité - Le code doit être facile à maintenir et à faire évoluer
- Testabilité - Le code doit être facile à tester
- Performance - Le code doit être efficace et optimisé
- Sécurité - Le code doit respecter les bonnes pratiques de sécurité
Structure du Projet
Le projet est structuré selon une architecture modulaire :
triosigno/
├── client/ # Frontend React/Next.js
│ ├── components/ # Composants UI réutilisables
│ ├── hooks/ # Hooks React personnalisés
│ ├── pages/ # Pages de l'application
│ ├── public/ # Fichiers statiques
│ ├── styles/ # Styles globaux et thèmes
│ └── utils/ # Utilitaires frontend
│
├── mobile/ # Mobile React/Next.js
│ ├── components/ # Composants UI réutilisables
│ ├── app/ # Pages de l'application
│ ├── services/ # Services for API calls and business logic
│ ├── constants/ # Constant values used in the application
│ ├── types/ # TypeScript type definitions
│ ├── styles/ # Styles globaux et thèmes
│ └── utils/ # Utilitaires mobile
│
├── server/ # Backend Node.js/Express
│ ├── api/ # Routes et contrôleurs API
│ ├── config/ # Configuration du serveur
│ ├── middleware/ # Middleware Express
│ ├── models/ # Modèles de données
│ ├── services/ # Services métier
│ └── utils/ # Utilitaires backend
│
├── shared/ # Code partagé frontend/backend
│ ├── constants/ # Constantes partagées
│ ├── types/ # Types TypeScript partagés
│ └── validation/ # Schémas de validation
│
├── ia/ # Composants d'intelligence artificielle
│ ├── models/ # Modèles d'IA entraînés
│ ├── training/ # Scripts d'entraînement
│ └── utils/ # Utilitaires pour l'IA
│
├── prisma/ # Schéma et migrations Prisma
│
├── tests/ # Tests globaux
│ ├── e2e/ # Tests end-to-end
│ └── integration/ # Tests d'intégration
│
└── scripts/ # Scripts utilitaires
Standards de Code
TypeScript
Nous utilisons TypeScript pour bénéficier du typage statique :
// Préférer les interfaces pour les structures de données
interface User {
id: string;
username: string;
email: string;
role: UserRole;
createdAt: Date;
}
// Utiliser des types pour les unions et les types utilitaires
type UserRole = "admin" | "teacher" | "student";
type UserWithoutId = Omit<User, "id">;
// Éviter any, préférer unknown si nécessaire
function processData(data: unknown): void {
if (isUser(data)) {
// Le type est maintenant User
console.log(data.username);
}
}
// Fonction de garde de type
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"username" in obj &&
"email" in obj
);
}
Nommage
Nous suivons ces conventions de nommage :
- PascalCase pour les classes, interfaces, types et composants React
- camelCase pour les variables, fonctions et propriétés
- UPPER_CASE pour les constantes
- kebab-case pour les fichiers CSS et les URLs
// Constantes
const MAX_RETRY_ATTEMPTS = 3;
// Variables
const userProfile = getUserProfile();
// Fonctions
function calculateProgress(userId: string): number {
// ...
}
// Classes
class AuthenticationService {
// ...
}
// Interfaces
interface SignProperties {
// ...
}
// Composants React
function UserProfileCard({ user }: UserProfileCardProps) {
// ...
}
Formatage
Nous utilisons ESLint et Prettier pour assurer la cohérence du code. Les fichiers de configuration sont inclus dans le projet.
Configuration ESLint :
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
]
}
}
Configuration Prettier :
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}
Commentaires
Nous suivons ces pratiques pour les commentaires :
- Commentaires JSDoc pour les fonctions et classes publiques
- Commentaires simples pour expliquer la logique complexe
- Éviter les commentaires qui répètent simplement le code
/**
* Calcule le niveau de maîtrise d'un signe pour un utilisateur.
*
* @param userId - L'identifiant de l'utilisateur
* @param signId - L'identifiant du signe
* @returns Le niveau de maîtrise entre 0 et 100
*/
async function calculateMasteryLevel(
userId: string,
signId: string
): Promise<number> {
// Récupérer l'historique des tentatives
const attempts = await getAttemptHistory(userId, signId);
// Utiliser une moyenne pondérée des 10 dernières tentatives
// où les tentatives récentes ont plus de poids
const recentAttempts = attempts.slice(-10);
// ...calcul complexe...
return masteryLevel;
}
Frontend
Composants React
Nous utilisons des composants fonctionnels avec des hooks :
import React, { useState, useEffect } from "react";
import { useUser } from "@/hooks/useUser";
import { Button } from "@/components/ui/Button";
interface LessonCardProps {
lessonId: string;
title: string;
description: string;
onStart: () => void;
}
export function LessonCard({
lessonId,
title,
description,
onStart,
}: LessonCardProps) {
const { user } = useUser();
const [progress, setProgress] = useState(0);
useEffect(() => {
// Charger la progression de l'utilisateur pour cette leçon
async function loadProgress() {
if (user) {
const userProgress = await fetchLessonProgress(user.id, lessonId);
setProgress(userProgress);
}
}
loadProgress();
}, [user, lessonId]);
return (
<div className="lesson-card">
<h3>{title}</h3>
<p>{description}</p>
<div className="progress-bar" style={{ width: `${progress}%` }} />
<Button onClick={onStart}>Commencer</Button>
</div>
);
}
Gestion d'État
Nous utilisons une combinaison de hooks React (useState, useReducer) et de React Context pour la gestion d'état :
// Contexte pour la gestion de l'authentification
import React, { createContext, useContext, useReducer, ReactNode } from "react";
interface AuthState {
isAuthenticated: boolean;
user: User | null;
loading: boolean;
error: string | null;
}
type AuthAction =
| { type: "LOGIN_START" }
| { type: "LOGIN_SUCCESS"; payload: User }
| { type: "LOGIN_FAILURE"; payload: string }
| { type: "LOGOUT" };
const AuthContext = createContext<
| {
state: AuthState;
dispatch: React.Dispatch<AuthAction>;
}
| undefined
>(undefined);
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case "LOGIN_START":
return { ...state, loading: true, error: null };
case "LOGIN_SUCCESS":
return {
...state,
isAuthenticated: true,
user: action.payload,
loading: false,
error: null,
};
case "LOGIN_FAILURE":
return {
...state,
isAuthenticated: false,
user: null,
loading: false,
error: action.payload,
};
case "LOGOUT":
return {
...state,
isAuthenticated: false,
user: null,
loading: false,
error: null,
};
default:
return state;
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, {
isAuthenticated: false,
user: null,
loading: false,
error: null,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
Styles
Nous utilisons une combinaison de CSS Modules et de Tailwind CSS :
// Exemple avec CSS Modules
import styles from "./Button.module.css";
export function Button({ children, variant = "primary", ...props }) {
return (
<button className={`${styles.button} ${styles[variant]}`} {...props}>
{children}
</button>
);
}
// Exemple avec Tailwind CSS
export function Card({ title, children }) {
return (
<div className="rounded-lg shadow-md p-4 bg-white dark:bg-gray-800">
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<div>{children}</div>
</div>
);
}
Backend
Architecture
Nous suivons une architecture en couches :
- Routes - Définissent les endpoints API
- Contrôleurs - Gèrent les requêtes et les réponses
- Services - Contiennent la logique métier
- Modèles - Définissent la structure des données
// Route
import express from "express";
import { getLessons, getLessonById } from "../controllers/lessonController";
import { authenticate } from "../middleware/auth";
const router = express.Router();
router.get("/lessons", authenticate, getLessons);
router.get("/lessons/:id", authenticate, getLessonById);
export default router;
// Contrôleur
import { Request, Response } from "express";
import { LessonService } from "../services/lessonService";
const lessonService = new LessonService();
export async function getLessons(req: Request, res: Response) {
try {
const lessons = await lessonService.getAllLessons();
res.json(lessons);
} catch (error) {
res.status(500).json({ message: "Failed to fetch lessons" });
}
}
// Service
import { prisma } from "../config/database";
export class LessonService {
async getAllLessons() {
return prisma.lesson.findMany({
orderBy: { order: "asc" },
include: {
module: true,
},
});
}
}
Gestion des Erreurs
Nous utilisons un middleware de gestion d'erreurs centralisé :
import { Request, Response, NextFunction } from "express";
// Types d'erreurs personnalisés
export class AppError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Unauthorized") {
super(message, 401);
}
}
// Middleware de gestion d'erreurs
export function errorHandler(
err: Error,
req: Request,
res: Response,
_next: NextFunction
) {
console.error(err);
if (err instanceof AppError) {
return res.status(err.statusCode).json({
status: "error",
message: err.message,
});
}
return res.status(500).json({
status: "error",
message: "Internal server error",
});
}
Validation
Nous utilisons Zod pour la validation des données :
import { z } from "zod";
import { Request, Response, NextFunction } from "express";
// Schéma de validation pour la création d'un utilisateur
const createUserSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
password: z.string().min(8),
firstName: z.string().optional(),
lastName: z.string().optional(),
});
// Middleware de validation
export function validateCreateUser(
req: Request,
res: Response,
next: NextFunction
) {
try {
createUserSchema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
status: "error",
message: "Validation failed",
errors: error.errors,
});
}
next(error);
}
}
Tests
Tests Unitaires
Nous utilisons Jest pour les tests unitaires :
import { calculateMasteryLevel } from "./masteryCalculator";
import { getAttemptHistory } from "./attemptService";
// Mock des dépendances
jest.mock("./attemptService", () => ({
getAttemptHistory: jest.fn(),
}));
describe("calculateMasteryLevel", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("should return 0 for no attempts", async () => {
(getAttemptHistory as jest.Mock).mockResolvedValue([]);
const result = await calculateMasteryLevel("user1", "sign1");
expect(result).toBe(0);
expect(getAttemptHistory).toHaveBeenCalledWith("user1", "sign1");
});
test("should calculate mastery based on recent attempts", async () => {
// Configure le mock pour retourner des données de test
(getAttemptHistory as jest.Mock).mockResolvedValue([
{ correct: true, date: new Date("2023-01-01") },
{ correct: false, date: new Date("2023-01-02") },
{ correct: true, date: new Date("2023-01-03") },
]);
const result = await calculateMasteryLevel("user1", "sign1");
// Vérifie que le résultat correspond à ce que nous attendons
expect(result).toBeGreaterThan(0);
expect(result).toBeLessThanOrEqual(100);
});
});
Tests d'Intégration
Nous utilisons Supertest pour tester les API :
import request from "supertest";
import { app } from "../app";
import { prisma } from "../config/database";
import { createUser, generateAuthToken } from "../utils/testHelpers";
describe("Lesson API", () => {
let authToken: string;
beforeAll(async () => {
// Créer un utilisateur de test et générer un token
const user = await createUser({
username: "testuser",
email: "test@example.com",
password: "password123",
});
authToken = generateAuthToken(user);
});
afterAll(async () => {
// Nettoyer la base de données
await prisma.user.deleteMany();
await prisma.$disconnect();
});
test("GET /api/lessons should return all lessons", async () => {
const response = await request(app)
.get("/api/lessons")
.set("Authorization", `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body).toBeInstanceOf(Array);
});
test("GET /api/lessons/:id should return a specific lesson", async () => {
// Créer une leçon de test
const lesson = await prisma.lesson.create({
data: {
title: "Test Lesson",
description: "Test description",
order: 1,
module: {
create: {
title: "Test Module",
description: "Test module description",
order: 1,
},
},
},
});
const response = await request(app)
.get(`/api/lessons/${lesson.id}`)
.set("Authorization", `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(lesson.id);
expect(response.body.title).toBe("Test Lesson");
});
});
Tests End-to-End
Nous utilisons Playwright pour les tests end-to-end :
import { test, expect } from "@playwright/test";
test.describe("User Authentication Flow", () => {
test("should allow user to sign up, log in, and access protected content", async ({
page,
}) => {
// 1. Navigate to the signup page
await page.goto("/signup");
// 2. Fill out and submit the signup form
await page.fill('input[name="username"]', `testuser-${Date.now()}`);
await page.fill('input[name="email"]', `test-${Date.now()}@example.com`);
await page.fill('input[name="password"]', "Password123!");
await page.fill('input[name="confirmPassword"]', "Password123!");
await page.click('button[type="submit"]');
// 3. Verify redirect to login page or dashboard
await expect(page).toHaveURL(/login|dashboard/);
// 4. If redirected to login, complete login
if (page.url().includes("login")) {
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', "Password123!");
await page.click('button[type="submit"]');
}
// 5. Verify access to dashboard
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator("h1")).toContainText("Dashboard");
// 6. Navigate to lessons page
await page.click("text=Lessons");
// 7. Verify access to lessons
await expect(page).toHaveURL(/lessons/);
await expect(page.locator("h1")).toContainText("Lessons");
});
});
Sécurité
Authentification
Nous utilisons JWT (JSON Web Tokens) pour l'authentification :
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { prisma } from "../config/database";
import { UnauthorizedError } from "../utils/errors";
export class AuthService {
async login(email: string, password: string) {
// Trouver l'utilisateur par email
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
throw new UnauthorizedError("Invalid credentials");
}
// Vérifier le mot de passe
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
throw new UnauthorizedError("Invalid credentials");
}
// Générer le token JWT
const token = jwt.sign(
{
id: user.id,
role: user.role,
},
process.env.JWT_SECRET!,
{ expiresIn: "7d" }
);
return {
token,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
},
};
}
}
Validation des Entrées
Nous validons toutes les entrées utilisateur :
import { z } from "zod";
// Schéma de validation pour les paramètres de requête
const lessonQuerySchema = z.object({
moduleId: z.string().uuid().optional(),
limit: z.coerce.number().min(1).max(100).optional().default(20),
page: z.coerce.number().min(1).optional().default(1),
});
// Validation et extraction des paramètres
export function validateLessonQuery(query: any) {
return lessonQuerySchema.parse(query);
}
Protection contre les Attaques Courantes
Nous mettons en place des protections contre les attaques courantes :
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import xss from "xss-clean";
import hpp from "hpp";
export function configureSecurityMiddleware(app) {
// Protection des en-têtes HTTP
app.use(helmet());
// Limitation de débit pour éviter les attaques par force brute
app.use(
"/api/auth",
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 requêtes par IP
message: "Too many login attempts, please try again later",
})
);
// Protection contre les attaques XSS
app.use(xss());
// Protection contre la pollution des paramètres HTTP
app.use(hpp());
}
Processus de Développement
Flux de Travail Git
Nous utilisons le modèle GitFlow :
- main - Code de production stable
- develop - Branche de développement principale
- feature/* - Branches pour les nouvelles fonctionnalités
- bugfix/* - Branches pour la correction de bugs
- release/* - Branches pour la préparation des versions
Processus de Pull Request
- Créer une branche à partir de develop
- Développer la fonctionnalité ou corriger le bug
- Exécuter les tests localement
- Créer une Pull Request vers develop
- Attendre la revue de code et l'exécution des tests CI
- Merger la Pull Request une fois approuvée
Messages de Commit
Nous suivons le format Conventional Commits :
type(scope): description concise
Description détaillée si nécessaire.
Références aux issues: #123, #456
Types courants :
- feat - Nouvelle fonctionnalité
- fix - Correction de bug
- docs - Documentation
- style - Formatage, point-virgules manquants, etc.
- refactor - Refactoring de code
- test - Tests
- chore - Tâches de maintenance
Versionnement
Nous suivons le versionnement sémantique (SemVer) :
- MAJOR - Changements incompatibles avec les versions précédentes
- MINOR - Ajouts de fonctionnalités compatibles avec les versions précédentes
- PATCH - Corrections de bugs compatibles avec les versions précédentes