
- Refonte complète du design avec système de panneaux latéraux rétractables - Ajout de templates de projets par domaine (recherche, informatique, mathématiques, etc.) - Implémentation système d'export PDF avec Puppeteer - Amélioration de l'API REST avec nouvelles routes d'export et templates - Ajout de JavaScript client pour interactions dynamiques - Configuration environnement étendue pour futures fonctionnalités IA - Amélioration responsive design et expérience utilisateur 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1122 lines
28 KiB
Markdown
1122 lines
28 KiB
Markdown
# Projet Informatique - Documentation Complète
|
|
|
|
## 1. Contexte et Vision
|
|
|
|
### 1.1 Contexte organisationnel
|
|
[Description de l'organisation, du département, de l'équipe]
|
|
|
|
### 1.2 Problématique métier
|
|
[Analyse détaillée des problèmes business à résoudre]
|
|
|
|
### 1.3 Vision produit
|
|
[Vision long-terme du produit et de son évolution]
|
|
|
|
### 1.4 Alignement stratégique
|
|
[Comment le projet s'inscrit dans la stratégie de l'entreprise]
|
|
|
|
## 2. Analyse des besoins approfondie
|
|
|
|
### 2.1 Parties prenantes
|
|
| Partie prenante | Rôle | Influence | Besoins | Contact |
|
|
|-----------------|------|-----------|---------|---------|
|
|
| Product Owner | | Élevée | | |
|
|
| Tech Lead | | Élevée | | |
|
|
| Utilisateurs finaux | | Moyenne | | |
|
|
|
|
### 2.2 User Stories détaillées
|
|
```gherkin
|
|
Feature: Authentification utilisateur
|
|
En tant qu'utilisateur
|
|
Je veux pouvoir me connecter de manière sécurisée
|
|
Afin d'accéder à mes données personnelles
|
|
|
|
Scenario: Connexion réussie
|
|
Given je suis sur la page de connexion
|
|
When je saisis mes identifiants corrects
|
|
Then je suis redirigé vers le dashboard
|
|
And je vois mon nom d'utilisateur affiché
|
|
|
|
Scenario: Connexion échouée
|
|
Given je suis sur la page de connexion
|
|
When je saisis des identifiants incorrects
|
|
Then je vois un message d'erreur
|
|
And je reste sur la page de connexion
|
|
```
|
|
|
|
### 2.3 Acceptance Criteria détaillés
|
|
[Critères d'acceptation pour chaque user story]
|
|
|
|
### 2.4 Contraintes détaillées
|
|
#### Contraintes techniques
|
|
- **Compatibilité** : IE11+, Chrome 80+, Firefox 75+, Safari 13+
|
|
- **Performance** : Temps de chargement < 2s, Core Web Vitals > 90
|
|
- **Accessibilité** : Conformité WCAG 2.1 AA
|
|
- **SEO** : Structure sémantique, balises meta, sitemap
|
|
|
|
#### Contraintes légales
|
|
- **RGPD** : Consentement, droit à l'oubli, portabilité
|
|
- **Sécurité** : ISO 27001, chiffrement bout-en-bout
|
|
- **Audit** : Traçabilité des actions, journalisation
|
|
|
|
## 3. Architecture complète
|
|
|
|
### 3.1 Architecture métier
|
|
[Domain-Driven Design, bounded contexts]
|
|
|
|
### 3.2 Architecture applicative
|
|
```mermaid
|
|
graph TB
|
|
A[Load Balancer] --> B[API Gateway]
|
|
B --> C[Auth Service]
|
|
B --> D[User Service]
|
|
B --> E[Content Service]
|
|
C --> F[(Auth DB)]
|
|
D --> G[(User DB)]
|
|
E --> H[(Content DB)]
|
|
E --> I[File Storage]
|
|
|
|
J[Frontend SPA] --> B
|
|
K[Mobile App] --> B
|
|
L[Admin Panel] --> B
|
|
```
|
|
|
|
### 3.3 Architecture technique détaillée
|
|
#### Microservices
|
|
```yaml
|
|
services:
|
|
auth-service:
|
|
image: auth-service:latest
|
|
environment:
|
|
- JWT_SECRET=${JWT_SECRET}
|
|
- DB_CONNECTION=${AUTH_DB_URL}
|
|
dependencies:
|
|
- postgres-auth
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
|
|
user-service:
|
|
image: user-service:latest
|
|
environment:
|
|
- DB_CONNECTION=${USER_DB_URL}
|
|
dependencies:
|
|
- postgres-users
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
|
|
```
|
|
|
|
### 3.4 Modèle de données complet
|
|
```sql
|
|
-- Système de permissions
|
|
CREATE TABLE roles (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) UNIQUE NOT NULL,
|
|
description TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE permissions (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) UNIQUE NOT NULL,
|
|
resource VARCHAR(100) NOT NULL,
|
|
action VARCHAR(100) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE role_permissions (
|
|
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (role_id, permission_id)
|
|
);
|
|
|
|
-- Audit trail
|
|
CREATE TABLE audit_logs (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id),
|
|
action VARCHAR(100) NOT NULL,
|
|
resource_type VARCHAR(100) NOT NULL,
|
|
resource_id VARCHAR(100),
|
|
old_values JSONB,
|
|
new_values JSONB,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Système de notifications
|
|
CREATE TABLE notifications (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
type VARCHAR(50) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
message TEXT,
|
|
read_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
## 4. Conception détaillée par couche
|
|
|
|
### 4.1 Couche présentation
|
|
#### Design System complet
|
|
```scss
|
|
// Tokens de design
|
|
$colors: (
|
|
primary: (
|
|
50: #f0f9ff,
|
|
100: #e0f2fe,
|
|
500: #0ea5e9,
|
|
900: #0c4a6e
|
|
),
|
|
semantic: (
|
|
success: #10b981,
|
|
warning: #f59e0b,
|
|
error: #ef4444,
|
|
info: #3b82f6
|
|
)
|
|
);
|
|
|
|
$typography: (
|
|
h1: (
|
|
font-size: 2.25rem,
|
|
line-height: 1.2,
|
|
font-weight: 700
|
|
),
|
|
body: (
|
|
font-size: 1rem,
|
|
line-height: 1.5,
|
|
font-weight: 400
|
|
)
|
|
);
|
|
|
|
$spacing: (
|
|
xs: 0.25rem,
|
|
sm: 0.5rem,
|
|
md: 1rem,
|
|
lg: 1.5rem,
|
|
xl: 3rem
|
|
);
|
|
```
|
|
|
|
#### Composants React avancés
|
|
```typescript
|
|
// Hook personnalisé pour la gestion d'état
|
|
export const useApiData = <T>(url: string, options?: RequestOptions) => {
|
|
const [data, setData] = useState<T | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await apiClient.get<T>(url, options);
|
|
setData(response.data);
|
|
} catch (err) {
|
|
setError(err as Error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [url]);
|
|
|
|
return { data, loading, error, refetch: fetchData };
|
|
};
|
|
|
|
// Composant de tableau réutilisable
|
|
interface DataTableProps<T> {
|
|
data: T[];
|
|
columns: ColumnDef<T>[];
|
|
loading?: boolean;
|
|
onRowClick?: (row: T) => void;
|
|
pagination?: PaginationOptions;
|
|
}
|
|
|
|
export function DataTable<T>({
|
|
data,
|
|
columns,
|
|
loading = false,
|
|
onRowClick,
|
|
pagination
|
|
}: DataTableProps<T>) {
|
|
// Implémentation avec react-table
|
|
}
|
|
```
|
|
|
|
### 4.2 Couche métier
|
|
#### Domain Services
|
|
```typescript
|
|
// Service métier pour la gestion des utilisateurs
|
|
export class UserDomainService {
|
|
constructor(
|
|
private userRepository: IUserRepository,
|
|
private emailService: IEmailService,
|
|
private auditService: IAuditService
|
|
) {}
|
|
|
|
async createUser(userData: CreateUserCommand): Promise<User> {
|
|
// Validation des règles métier
|
|
await this.validateUserCreation(userData);
|
|
|
|
// Création de l'utilisateur
|
|
const user = await this.userRepository.create({
|
|
...userData,
|
|
status: UserStatus.PENDING_VERIFICATION,
|
|
createdAt: new Date()
|
|
});
|
|
|
|
// Envoi email de vérification
|
|
await this.emailService.sendVerificationEmail(user.email);
|
|
|
|
// Audit
|
|
await this.auditService.log({
|
|
action: 'USER_CREATED',
|
|
userId: user.id,
|
|
details: { email: user.email }
|
|
});
|
|
|
|
return user;
|
|
}
|
|
|
|
private async validateUserCreation(userData: CreateUserCommand): Promise<void> {
|
|
// Vérification unicité email
|
|
const existingUser = await this.userRepository.findByEmail(userData.email);
|
|
if (existingUser) {
|
|
throw new DomainError('EMAIL_ALREADY_EXISTS', 'Cet email est déjà utilisé');
|
|
}
|
|
|
|
// Validation format email
|
|
if (!this.isValidEmail(userData.email)) {
|
|
throw new DomainError('INVALID_EMAIL_FORMAT', 'Format email invalide');
|
|
}
|
|
|
|
// Validation force mot de passe
|
|
if (!this.isStrongPassword(userData.password)) {
|
|
throw new DomainError('WEAK_PASSWORD', 'Le mot de passe n\'est pas assez fort');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.3 Couche données
|
|
#### Repositories avec patterns avancés
|
|
```typescript
|
|
// Pattern Repository avec Unit of Work
|
|
export class UserRepository implements IUserRepository {
|
|
constructor(private db: Database) {}
|
|
|
|
async findById(id: string): Promise<User | null> {
|
|
const query = `
|
|
SELECT u.*, r.name as role_name
|
|
FROM users u
|
|
LEFT JOIN roles r ON u.role_id = r.id
|
|
WHERE u.id = $1 AND u.deleted_at IS NULL
|
|
`;
|
|
|
|
const result = await this.db.query(query, [id]);
|
|
return result.rows[0] ? this.mapToUser(result.rows[0]) : null;
|
|
}
|
|
|
|
async findWithPagination(
|
|
filters: UserFilters,
|
|
pagination: PaginationOptions
|
|
): Promise<PaginatedResult<User>> {
|
|
const whereClause = this.buildWhereClause(filters);
|
|
const orderClause = this.buildOrderClause(pagination.sortBy, pagination.sortOrder);
|
|
|
|
const [dataResult, countResult] = await Promise.all([
|
|
this.db.query(`
|
|
SELECT u.*, r.name as role_name
|
|
FROM users u
|
|
LEFT JOIN roles r ON u.role_id = r.id
|
|
${whereClause}
|
|
${orderClause}
|
|
LIMIT $1 OFFSET $2
|
|
`, [pagination.limit, pagination.offset]),
|
|
|
|
this.db.query(`
|
|
SELECT COUNT(*) as total
|
|
FROM users u
|
|
${whereClause}
|
|
`)
|
|
]);
|
|
|
|
return {
|
|
data: dataResult.rows.map(this.mapToUser),
|
|
total: parseInt(countResult.rows[0].total),
|
|
page: pagination.page,
|
|
limit: pagination.limit
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## 5. Qualité et tests approfondis
|
|
|
|
### 5.1 Stratégie de tests complète
|
|
```typescript
|
|
// Tests unitaires avec mocks
|
|
describe('UserDomainService', () => {
|
|
let userService: UserDomainService;
|
|
let mockUserRepository: jest.Mocked<IUserRepository>;
|
|
let mockEmailService: jest.Mocked<IEmailService>;
|
|
|
|
beforeEach(() => {
|
|
mockUserRepository = {
|
|
create: jest.fn(),
|
|
findByEmail: jest.fn(),
|
|
} as any;
|
|
|
|
mockEmailService = {
|
|
sendVerificationEmail: jest.fn(),
|
|
} as any;
|
|
|
|
userService = new UserDomainService(
|
|
mockUserRepository,
|
|
mockEmailService,
|
|
mockAuditService
|
|
);
|
|
});
|
|
|
|
describe('createUser', () => {
|
|
it('should create user successfully', async () => {
|
|
// Arrange
|
|
const userData = {
|
|
email: 'test@example.com',
|
|
password: 'StrongPassword123!',
|
|
firstName: 'John',
|
|
lastName: 'Doe'
|
|
};
|
|
|
|
mockUserRepository.findByEmail.mockResolvedValue(null);
|
|
mockUserRepository.create.mockResolvedValue(mockUser);
|
|
|
|
// Act
|
|
const result = await userService.createUser(userData);
|
|
|
|
// Assert
|
|
expect(mockUserRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
email: userData.email,
|
|
status: UserStatus.PENDING_VERIFICATION
|
|
})
|
|
);
|
|
expect(mockEmailService.sendVerificationEmail).toHaveBeenCalledWith(userData.email);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 5.2 Tests d'intégration
|
|
```typescript
|
|
// Tests d'API avec base de données de test
|
|
describe('Users API Integration', () => {
|
|
let app: Application;
|
|
let testDb: TestDatabase;
|
|
|
|
beforeAll(async () => {
|
|
testDb = await TestDatabase.create();
|
|
app = createApp({ database: testDb });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.cleanup();
|
|
});
|
|
|
|
describe('POST /api/users', () => {
|
|
it('should create user and return 201', async () => {
|
|
// Arrange
|
|
const userData = {
|
|
email: 'test@example.com',
|
|
password: 'StrongPassword123!',
|
|
firstName: 'John',
|
|
lastName: 'Doe'
|
|
};
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/users')
|
|
.send(userData)
|
|
.expect(201);
|
|
|
|
// Assert
|
|
expect(response.body.data).toMatchObject({
|
|
email: userData.email,
|
|
firstName: userData.firstName,
|
|
lastName: userData.lastName,
|
|
status: 'PENDING_VERIFICATION'
|
|
});
|
|
|
|
// Vérifier en base
|
|
const userInDb = await testDb.query(
|
|
'SELECT * FROM users WHERE email = $1',
|
|
[userData.email]
|
|
);
|
|
expect(userInDb.rows).toHaveLength(1);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 5.3 Tests E2E avec Cypress
|
|
```typescript
|
|
// Tests end-to-end
|
|
describe('User Management Flow', () => {
|
|
beforeEach(() => {
|
|
cy.resetDatabase();
|
|
cy.seedTestData();
|
|
});
|
|
|
|
it('should allow admin to create, edit and delete users', () => {
|
|
// Login as admin
|
|
cy.login('admin@example.com', 'password');
|
|
|
|
// Navigate to users page
|
|
cy.visit('/admin/users');
|
|
cy.get('[data-testid="users-page"]').should('be.visible');
|
|
|
|
// Create new user
|
|
cy.get('[data-testid="create-user-btn"]').click();
|
|
cy.get('[data-testid="user-form"]').should('be.visible');
|
|
|
|
cy.get('[data-testid="email-input"]').type('newuser@example.com');
|
|
cy.get('[data-testid="firstname-input"]').type('New');
|
|
cy.get('[data-testid="lastname-input"]').type('User');
|
|
cy.get('[data-testid="role-select"]').select('User');
|
|
|
|
cy.get('[data-testid="submit-btn"]').click();
|
|
|
|
// Verify user was created
|
|
cy.get('[data-testid="success-message"]')
|
|
.should('contain', 'Utilisateur créé avec succès');
|
|
|
|
cy.get('[data-testid="users-table"]')
|
|
.should('contain', 'newuser@example.com');
|
|
});
|
|
});
|
|
```
|
|
|
|
## 6. Sécurité approfondie
|
|
|
|
### 6.1 Authentification et autorisation
|
|
```typescript
|
|
// JWT avec refresh tokens
|
|
export class AuthService {
|
|
async authenticateUser(credentials: LoginCredentials): Promise<AuthResult> {
|
|
// Validation et authentification
|
|
const user = await this.validateCredentials(credentials);
|
|
|
|
// Génération des tokens
|
|
const accessToken = this.generateAccessToken(user);
|
|
const refreshToken = this.generateRefreshToken(user);
|
|
|
|
// Stockage du refresh token
|
|
await this.storeRefreshToken(user.id, refreshToken);
|
|
|
|
// Audit de connexion
|
|
await this.auditService.logAuth(user.id, 'LOGIN_SUCCESS');
|
|
|
|
return {
|
|
user,
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn: this.accessTokenTTL
|
|
};
|
|
}
|
|
|
|
async refreshAccessToken(refreshToken: string): Promise<RefreshResult> {
|
|
// Validation du refresh token
|
|
const tokenData = await this.validateRefreshToken(refreshToken);
|
|
|
|
// Génération nouveau access token
|
|
const newAccessToken = this.generateAccessToken(tokenData.user);
|
|
|
|
return {
|
|
accessToken: newAccessToken,
|
|
expiresIn: this.accessTokenTTL
|
|
};
|
|
}
|
|
}
|
|
|
|
// Middleware d'autorisation basé sur les rôles
|
|
export const requirePermission = (resource: string, action: string) => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
const user = req.user;
|
|
|
|
const hasPermission = await permissionService.userHasPermission(
|
|
user.id,
|
|
resource,
|
|
action
|
|
);
|
|
|
|
if (!hasPermission) {
|
|
return res.status(403).json({
|
|
error: {
|
|
code: 'INSUFFICIENT_PERMISSIONS',
|
|
message: 'Permission insuffisante pour cette action'
|
|
}
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
```
|
|
|
|
### 6.2 Validation et sanitisation
|
|
```typescript
|
|
// Schémas de validation avec Joi
|
|
const userCreationSchema = Joi.object({
|
|
email: Joi.string()
|
|
.email()
|
|
.max(255)
|
|
.required()
|
|
.messages({
|
|
'string.email': 'Format email invalide',
|
|
'string.max': 'Email trop long (max 255 caractères)',
|
|
'any.required': 'Email obligatoire'
|
|
}),
|
|
|
|
password: Joi.string()
|
|
.min(8)
|
|
.max(128)
|
|
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
|
|
.required()
|
|
.messages({
|
|
'string.min': 'Mot de passe trop court (min 8 caractères)',
|
|
'string.pattern.base': 'Le mot de passe doit contenir au moins: 1 minuscule, 1 majuscule, 1 chiffre, 1 caractère spécial'
|
|
}),
|
|
|
|
firstName: Joi.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(100)
|
|
.required(),
|
|
|
|
lastName: Joi.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(100)
|
|
.required()
|
|
});
|
|
|
|
// Middleware de validation
|
|
export const validateRequest = (schema: Joi.ObjectSchema) => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
const { error, value } = schema.validate(req.body, {
|
|
abortEarly: false,
|
|
stripUnknown: true
|
|
});
|
|
|
|
if (error) {
|
|
return res.status(400).json({
|
|
error: {
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'Données invalides',
|
|
details: error.details.map(detail => ({
|
|
field: detail.path.join('.'),
|
|
message: detail.message
|
|
}))
|
|
}
|
|
});
|
|
}
|
|
|
|
req.body = value;
|
|
next();
|
|
};
|
|
};
|
|
```
|
|
|
|
## 7. Performance et monitoring
|
|
|
|
### 7.1 Optimisations performance
|
|
```typescript
|
|
// Cache Redis pour améliorer les performances
|
|
export class CachedUserService {
|
|
constructor(
|
|
private userService: UserDomainService,
|
|
private cache: RedisClient
|
|
) {}
|
|
|
|
async getUser(id: string): Promise<User | null> {
|
|
// Tentative de récupération depuis le cache
|
|
const cacheKey = `user:${id}`;
|
|
const cachedUser = await this.cache.get(cacheKey);
|
|
|
|
if (cachedUser) {
|
|
return JSON.parse(cachedUser);
|
|
}
|
|
|
|
// Récupération depuis la base de données
|
|
const user = await this.userService.getUser(id);
|
|
|
|
if (user) {
|
|
// Mise en cache pour 1 heure
|
|
await this.cache.setex(cacheKey, 3600, JSON.stringify(user));
|
|
}
|
|
|
|
return user;
|
|
}
|
|
}
|
|
|
|
// Pagination et filtrage optimisés
|
|
export class UserQueryService {
|
|
async searchUsers(filters: UserSearchFilters): Promise<PaginatedResult<User>> {
|
|
// Construction de la requête avec index appropriés
|
|
const query = this.queryBuilder
|
|
.select([
|
|
'u.id',
|
|
'u.email',
|
|
'u.first_name',
|
|
'u.last_name',
|
|
'r.name as role_name'
|
|
])
|
|
.from('users', 'u')
|
|
.leftJoin('roles', 'r', 'u.role_id = r.id')
|
|
.where('u.deleted_at IS NULL');
|
|
|
|
// Application des filtres
|
|
if (filters.email) {
|
|
query.andWhere('u.email ILIKE :email', { email: `%${filters.email}%` });
|
|
}
|
|
|
|
if (filters.role) {
|
|
query.andWhere('r.name = :role', { role: filters.role });
|
|
}
|
|
|
|
if (filters.status) {
|
|
query.andWhere('u.status = :status', { status: filters.status });
|
|
}
|
|
|
|
// Pagination
|
|
const total = await query.getCount();
|
|
const users = await query
|
|
.offset((filters.page - 1) * filters.limit)
|
|
.limit(filters.limit)
|
|
.orderBy('u.created_at', 'DESC')
|
|
.getMany();
|
|
|
|
return {
|
|
data: users,
|
|
total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(total / filters.limit)
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7.2 Monitoring et observabilité
|
|
```typescript
|
|
// Métriques custom avec Prometheus
|
|
import { register, Counter, Histogram, Gauge } from 'prom-client';
|
|
|
|
export class MetricsService {
|
|
private httpRequestDuration = new Histogram({
|
|
name: 'http_request_duration_seconds',
|
|
help: 'Duration of HTTP requests in seconds',
|
|
labelNames: ['method', 'route', 'status_code'],
|
|
buckets: [0.1, 0.5, 1, 2, 5]
|
|
});
|
|
|
|
private httpRequestsTotal = new Counter({
|
|
name: 'http_requests_total',
|
|
help: 'Total number of HTTP requests',
|
|
labelNames: ['method', 'route', 'status_code']
|
|
});
|
|
|
|
private activeConnections = new Gauge({
|
|
name: 'websocket_connections_active',
|
|
help: 'Number of active WebSocket connections'
|
|
});
|
|
|
|
recordHttpRequest(method: string, route: string, statusCode: number, duration: number) {
|
|
this.httpRequestsTotal.inc({ method, route, status_code: statusCode });
|
|
this.httpRequestDuration.observe({ method, route, status_code: statusCode }, duration);
|
|
}
|
|
|
|
incrementActiveConnections() {
|
|
this.activeConnections.inc();
|
|
}
|
|
|
|
decrementActiveConnections() {
|
|
this.activeConnections.dec();
|
|
}
|
|
}
|
|
|
|
// Middleware de monitoring
|
|
export const monitoringMiddleware = (metricsService: MetricsService) => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
const start = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const duration = (Date.now() - start) / 1000;
|
|
metricsService.recordHttpRequest(
|
|
req.method,
|
|
req.route?.path || req.path,
|
|
res.statusCode,
|
|
duration
|
|
);
|
|
});
|
|
|
|
next();
|
|
};
|
|
};
|
|
```
|
|
|
|
## 8. DevOps et déploiement
|
|
|
|
### 8.1 Pipeline CI/CD complet
|
|
```yaml
|
|
# .github/workflows/ci-cd.yml
|
|
name: CI/CD Pipeline
|
|
|
|
on:
|
|
push:
|
|
branches: [main, develop]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:13
|
|
env:
|
|
POSTGRES_PASSWORD: postgres
|
|
POSTGRES_DB: test_db
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run linting
|
|
run: npm run lint
|
|
|
|
- name: Run type checking
|
|
run: npm run type-check
|
|
|
|
- name: Run unit tests
|
|
run: npm run test:unit
|
|
env:
|
|
CI: true
|
|
|
|
- name: Run integration tests
|
|
run: npm run test:integration
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
|
|
|
- name: Run E2E tests
|
|
run: npm run test:e2e
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
|
|
|
- name: Generate coverage report
|
|
run: npm run test:coverage
|
|
|
|
- name: Upload coverage to Codecov
|
|
uses: codecov/codecov-action@v3
|
|
|
|
security-scan:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Run security audit
|
|
run: npm audit --audit-level=high
|
|
|
|
- name: Run SAST scan
|
|
uses: securecodewarrior/github-action-add-sarif@v1
|
|
with:
|
|
sarif-file: 'security-scan.sarif'
|
|
|
|
build:
|
|
needs: [test, security-scan]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Build Docker images
|
|
run: |
|
|
docker build -t myapp/frontend:${{ github.sha }} ./frontend
|
|
docker build -t myapp/backend:${{ github.sha }} ./backend
|
|
|
|
- name: Push to registry
|
|
run: |
|
|
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
|
docker push myapp/frontend:${{ github.sha }}
|
|
docker push myapp/backend:${{ github.sha }}
|
|
|
|
deploy-staging:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: github.ref == 'refs/heads/develop'
|
|
steps:
|
|
- name: Deploy to staging
|
|
run: |
|
|
# Déploiement sur l'environnement de staging
|
|
kubectl set image deployment/frontend frontend=myapp/frontend:${{ github.sha }}
|
|
kubectl set image deployment/backend backend=myapp/backend:${{ github.sha }}
|
|
|
|
deploy-production:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: github.ref == 'refs/heads/main'
|
|
environment: production
|
|
steps:
|
|
- name: Deploy to production
|
|
run: |
|
|
# Déploiement sur l'environnement de production
|
|
kubectl set image deployment/frontend frontend=myapp/frontend:${{ github.sha }}
|
|
kubectl set image deployment/backend backend=myapp/backend:${{ github.sha }}
|
|
```
|
|
|
|
### 8.2 Configuration Kubernetes
|
|
```yaml
|
|
# k8s/production/deployment.yml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: backend
|
|
namespace: production
|
|
spec:
|
|
replicas: 3
|
|
strategy:
|
|
type: RollingUpdate
|
|
rollingUpdate:
|
|
maxUnavailable: 1
|
|
maxSurge: 1
|
|
selector:
|
|
matchLabels:
|
|
app: backend
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: backend
|
|
spec:
|
|
containers:
|
|
- name: backend
|
|
image: myapp/backend:latest
|
|
ports:
|
|
- containerPort: 3000
|
|
env:
|
|
- name: NODE_ENV
|
|
value: "production"
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: app-secrets
|
|
key: database-url
|
|
- name: JWT_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: app-secrets
|
|
key: jwt-secret
|
|
resources:
|
|
requests:
|
|
memory: "256Mi"
|
|
cpu: "250m"
|
|
limits:
|
|
memory: "512Mi"
|
|
cpu: "500m"
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 3000
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ready
|
|
port: 3000
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: backend-service
|
|
namespace: production
|
|
spec:
|
|
selector:
|
|
app: backend
|
|
ports:
|
|
- port: 80
|
|
targetPort: 3000
|
|
type: ClusterIP
|
|
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: app-ingress
|
|
namespace: production
|
|
annotations:
|
|
kubernetes.io/ingress.class: nginx
|
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
nginx.ingress.kubernetes.io/rate-limit: "100"
|
|
spec:
|
|
tls:
|
|
- hosts:
|
|
- api.myapp.com
|
|
secretName: api-tls
|
|
rules:
|
|
- host: api.myapp.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: backend-service
|
|
port:
|
|
number: 80
|
|
```
|
|
|
|
## 9. Évolution et maintenance
|
|
|
|
### 9.1 Roadmap technique
|
|
#### Q1 2025
|
|
- [ ] Migration vers TypeScript 5.0
|
|
- [ ] Upgrade React 18 avec Concurrent Features
|
|
- [ ] Implémentation GraphQL
|
|
- [ ] Microservices avec Service Mesh
|
|
|
|
#### Q2 2025
|
|
- [ ] Migration cloud-native (Kubernetes)
|
|
- [ ] Intégration IA/ML pour recommandations
|
|
- [ ] PWA avec support offline
|
|
- [ ] Monitoring avancé avec OpenTelemetry
|
|
|
|
#### Q3 2025
|
|
- [ ] Architecture event-driven avec Kafka
|
|
- [ ] Support multi-tenant
|
|
- [ ] API versioning complet
|
|
- [ ] Edge computing avec CDN
|
|
|
|
### 9.2 Plan de migration
|
|
```typescript
|
|
// Migration de données avec versioning
|
|
export class MigrationService {
|
|
async migrateToV2(): Promise<void> {
|
|
const batch = await this.getMigrationBatch();
|
|
|
|
for (const user of batch) {
|
|
try {
|
|
// Transformation des données
|
|
const v2User = this.transformUserToV2(user);
|
|
|
|
// Sauvegarde avec nouvelle structure
|
|
await this.saveV2User(v2User);
|
|
|
|
// Marquer comme migré
|
|
await this.markAsMigrated(user.id);
|
|
|
|
} catch (error) {
|
|
await this.logMigrationError(user.id, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private transformUserToV2(v1User: V1User): V2User {
|
|
return {
|
|
id: v1User.id,
|
|
profile: {
|
|
email: v1User.email,
|
|
firstName: v1User.firstName,
|
|
lastName: v1User.lastName,
|
|
avatar: v1User.avatar
|
|
},
|
|
preferences: {
|
|
language: v1User.language || 'fr',
|
|
timezone: v1User.timezone || 'Europe/Paris',
|
|
notifications: {
|
|
email: v1User.emailNotifications ?? true,
|
|
push: false
|
|
}
|
|
},
|
|
createdAt: v1User.createdAt,
|
|
updatedAt: new Date()
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## 10. Documentation et formation
|
|
|
|
### 10.1 Documentation technique complète
|
|
- [ ] Architecture Decision Records (ADR)
|
|
- [ ] API Documentation (OpenAPI 3.0)
|
|
- [ ] Code documentation (JSDoc/TSDoc)
|
|
- [ ] Runbooks opérationnels
|
|
- [ ] Guide de contribution
|
|
|
|
### 10.2 Documentation utilisateur
|
|
- [ ] Guide d'onboarding
|
|
- [ ] Manuel utilisateur complet
|
|
- [ ] FAQ interactive
|
|
- [ ] Tutoriels vidéo
|
|
- [ ] Base de connaissances
|
|
|
|
### 10.3 Formation équipe
|
|
- [ ] Sessions de formation technique
|
|
- [ ] Code reviews guidelines
|
|
- [ ] Best practices documentation
|
|
- [ ] Mentoring plan
|
|
- [ ] Certification interne
|
|
|
|
## 11. Mesures de succès
|
|
|
|
### 11.1 KPIs techniques
|
|
- **Performance** : P95 response time < 500ms
|
|
- **Disponibilité** : SLA 99.9%
|
|
- **Qualité** : Code coverage > 90%
|
|
- **Sécurité** : 0 vulnérabilité critique
|
|
|
|
### 11.2 KPIs business
|
|
- **Adoption** : 80% des utilisateurs actifs mensuels
|
|
- **Satisfaction** : NPS > 50
|
|
- **Support** : < 2% de tickets critiques
|
|
- **ROI** : Retour sur investissement en 18 mois
|
|
|
|
## 12. Journal détaillé du projet
|
|
|
|
### [Date] - Kickoff projet
|
|
[Notes sur le lancement, équipe, planning]
|
|
|
|
### [Date] - Architecture review
|
|
[Décisions architecturales, trade-offs, consensus équipe]
|
|
|
|
### [Date] - Milestone 1
|
|
[Réalisations, défis, ajustements nécessaires]
|
|
|
|
### [Date] - Production deployment
|
|
[Métriques de lancement, feedback utilisateurs, actions correctives]
|
|
|
|
## 13. Annexes techniques
|
|
|
|
### Annexe A : Schémas de base de données complets
|
|
### Annexe B : Diagrammes d'architecture
|
|
### Annexe C : Spécifications d'API détaillées
|
|
### Annexe D : Guide de déploiement
|
|
### Annexe E : Procédures de récupération après incident |