1218 lines
32 KiB
Markdown
1218 lines
32 KiB
Markdown
# 🔍 AUDIT COMPLET - PROJET NEAH
|
||
## Next.js 16.1.1 (App Router) - Audit Performance, Qualité, Sécurité & RGPD
|
||
|
||
**Date de l'audit :** $(date)
|
||
**Version Next.js :** 16.1.1
|
||
**Architecture :** App Router
|
||
**TypeScript :** ✅ Activé
|
||
|
||
---
|
||
|
||
## 📊 RÉSUMÉ EXÉCUTIF
|
||
|
||
| Catégorie | Score | Statut |
|
||
|-----------|-------|--------|
|
||
| **Performance** | 55/100 | ⚠️ À améliorer |
|
||
| **Qualité du Code** | 60/100 | ⚠️ Intermédiaire |
|
||
| **Sécurité** | 50/100 | ⚠️ Critique |
|
||
| **RGPD** | 40/100 | 🚫 Non conforme |
|
||
| **SCORE GLOBAL** | **51/100** | ⚠️ **Prêt avec correctifs critiques** |
|
||
|
||
### 🎯 Verdict Final
|
||
|
||
**⚠️ PRÊT AVEC CORRECTIFS CRITIQUES**
|
||
|
||
Le projet nécessite des correctifs importants avant la mise en production, notamment sur la sécurité et la conformité RGPD.
|
||
|
||
---
|
||
|
||
## 1️⃣ PERFORMANCE & OPTIMISATION
|
||
|
||
### ✅ Points Forts
|
||
|
||
1. **Next.js 16.1.1** - Version récente avec App Router
|
||
2. **next/font** - Utilisation correcte de `Inter` avec `next/font/google`
|
||
3. **Dynamic imports** - Utilisation de `dynamic()` et `import()` pour le code splitting
|
||
- Exemple : `components/observatory/observatory-map.tsx` utilise `dynamic()` pour Leaflet
|
||
- Plusieurs imports dynamiques dans les services (caldav-sync, microsoft-calendar-sync)
|
||
4. **Redis caching** - Système de cache Redis implémenté
|
||
5. **Health check endpoint** - `/api/health` pour monitoring
|
||
|
||
### 🚨 Problèmes Détectés
|
||
|
||
#### 1. **Images non optimisées** ⚠️ CRITIQUE
|
||
```javascript
|
||
// next.config.mjs ligne 9-11
|
||
images: {
|
||
unoptimized: true, // ❌ DÉSACTIVE l'optimisation Next.js
|
||
}
|
||
```
|
||
**Impact :**
|
||
- Pas de lazy loading automatique
|
||
- Pas de conversion WebP/AVIF
|
||
- Pas de responsive images
|
||
- Augmentation du poids des pages
|
||
|
||
**Exemples dans le code :**
|
||
- `app/missions/page.tsx` ligne 238 : `<img>` au lieu de `<Image>`
|
||
- `app/mission-tab/[missionId]/page.tsx` ligne 222 : `<img>` natif
|
||
- Seul `components/podcast.tsx` utilise `next/image` correctement
|
||
|
||
#### 2. **Page principale en Client Component** ⚠️ IMPORTANT
|
||
```typescript
|
||
// app/page.tsx ligne 1
|
||
"use client"; // ❌ Toute la page est client-side
|
||
```
|
||
**Impact :**
|
||
- Pas de SSR pour la page d'accueil
|
||
- Hydration complète côté client
|
||
- Pas de streaming
|
||
- TTFB plus élevé
|
||
|
||
#### 3. **Pas de metadata dans le layout principal** ⚠️ IMPORTANT
|
||
```typescript
|
||
// app/layout.tsx
|
||
// ❌ Pas de metadata exportée
|
||
```
|
||
**Impact :**
|
||
- Pas de SEO de base
|
||
- Pas d'Open Graph
|
||
- Pas de description
|
||
|
||
#### 4. **Utilisation excessive de localStorage/sessionStorage** ⚠️ MOYEN
|
||
- 72 occurrences de `localStorage`/`sessionStorage`
|
||
- Stockage de données utilisateur sans consentement explicite
|
||
- Risque RGPD (voir section 3)
|
||
|
||
#### 5. **Pas de Suspense boundaries** ⚠️ MOYEN
|
||
- Aucun fichier `loading.tsx` trouvé
|
||
- Pas de gestion de loading states avec Suspense
|
||
- Spinners manuels dans les composants
|
||
|
||
#### 6. **Pas de error.tsx** ⚠️ MOYEN
|
||
- Aucun fichier `error.tsx` trouvé
|
||
- Pas de gestion d'erreurs au niveau des routes
|
||
- Gestion d'erreurs uniquement dans les composants
|
||
|
||
#### 7. **Bundle size non optimisé** ⚠️ MOYEN
|
||
- Beaucoup de dépendances lourdes chargées
|
||
- Pas de tree-shaking visible
|
||
- Pas d'analyse de bundle size
|
||
|
||
### 📋 Recommandations Actionnables
|
||
|
||
#### 🔴 CRITIQUE - Activer l'optimisation d'images
|
||
|
||
```typescript
|
||
// next.config.mjs
|
||
const nextConfig = {
|
||
images: {
|
||
unoptimized: false, // ✅ Activer
|
||
formats: ['image/avif', 'image/webp'],
|
||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||
remotePatterns: [
|
||
{
|
||
protocol: 'https',
|
||
hostname: '**',
|
||
},
|
||
],
|
||
},
|
||
};
|
||
```
|
||
|
||
Remplacer tous les `<img>` par `<Image>` :
|
||
```typescript
|
||
// Avant
|
||
<img src={mission.logoUrl} alt={mission.name} />
|
||
|
||
// Après
|
||
import Image from 'next/image';
|
||
<Image
|
||
src={mission.logoUrl}
|
||
alt={mission.name}
|
||
width={192}
|
||
height={192}
|
||
className="w-full h-full object-cover rounded-md"
|
||
loading="lazy"
|
||
/>
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Convertir la page principale en Server Component
|
||
|
||
```typescript
|
||
// app/page.tsx - Nouvelle version
|
||
import { getServerSession } from "next-auth/next";
|
||
import { authOptions } from "@/app/api/auth/options";
|
||
import { QuoteCard } from "@/components/quote-card";
|
||
import { Calendar } from "@/components/calendar";
|
||
import { News } from "@/components/news";
|
||
import { Duties } from "@/components/flow";
|
||
import { Email } from "@/components/email";
|
||
import { Parole } from "@/components/parole";
|
||
import { Suspense } from "react";
|
||
|
||
export default async function Home() {
|
||
const session = await getServerSession(authOptions);
|
||
|
||
if (!session) {
|
||
redirect("/signin");
|
||
}
|
||
|
||
return (
|
||
<main className="h-screen overflow-auto">
|
||
<div className="container mx-auto p-4 mt-12">
|
||
<div className="grid grid-cols-12 gap-4 mb-4">
|
||
<Suspense fallback={<QuoteCardSkeleton />}>
|
||
<QuoteCard />
|
||
</Suspense>
|
||
<Suspense fallback={<CalendarSkeleton />}>
|
||
<Calendar />
|
||
</Suspense>
|
||
<Suspense fallback={<NewsSkeleton />}>
|
||
<News />
|
||
</Suspense>
|
||
<Suspense fallback={<DutiesSkeleton />}>
|
||
<Duties />
|
||
</Suspense>
|
||
</div>
|
||
<div className="grid grid-cols-12 gap-4">
|
||
<Suspense fallback={<EmailSkeleton />}>
|
||
<Email />
|
||
</Suspense>
|
||
<Suspense fallback={<ParoleSkeleton />}>
|
||
<Parole />
|
||
</Suspense>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Ajouter metadata au layout
|
||
|
||
```typescript
|
||
// app/layout.tsx
|
||
import type { Metadata } from "next";
|
||
|
||
export const metadata: Metadata = {
|
||
title: {
|
||
default: "NEAH - Plateforme de gestion",
|
||
template: "%s | NEAH"
|
||
},
|
||
description: "Plateforme de gestion NEAH",
|
||
keywords: ["NEAH", "gestion", "missions"],
|
||
authors: [{ name: "NEAH Team" }],
|
||
openGraph: {
|
||
type: "website",
|
||
locale: "fr_FR",
|
||
url: process.env.NEXTAUTH_URL,
|
||
siteName: "NEAH",
|
||
title: "NEAH - Plateforme de gestion",
|
||
description: "Plateforme de gestion NEAH",
|
||
},
|
||
robots: {
|
||
index: true,
|
||
follow: true,
|
||
},
|
||
};
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Créer loading.tsx et error.tsx
|
||
|
||
```typescript
|
||
// app/loading.tsx
|
||
export default function Loading() {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// app/error.tsx
|
||
'use client';
|
||
|
||
import { useEffect } from 'react';
|
||
|
||
export default function Error({
|
||
error,
|
||
reset,
|
||
}: {
|
||
error: Error & { digest?: string };
|
||
reset: () => void;
|
||
}) {
|
||
useEffect(() => {
|
||
console.error(error);
|
||
}, [error]);
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||
<h2 className="text-2xl font-bold mb-4">Une erreur est survenue</h2>
|
||
<button
|
||
onClick={reset}
|
||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||
>
|
||
Réessayer
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 🟢 MOYEN - Optimiser le bundle size
|
||
|
||
```bash
|
||
# Ajouter à package.json
|
||
"analyze": "ANALYZE=true next build"
|
||
```
|
||
|
||
Installer `@next/bundle-analyzer` :
|
||
```bash
|
||
npm install --save-dev @next/bundle-analyzer
|
||
```
|
||
|
||
```typescript
|
||
// next.config.mjs
|
||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||
enabled: process.env.ANALYZE === 'true',
|
||
});
|
||
|
||
module.exports = withBundleAnalyzer(nextConfig);
|
||
```
|
||
|
||
---
|
||
|
||
## 2️⃣ BONNES PRATIQUES NEXT.JS & CODE QUALITY
|
||
|
||
### ✅ Points Forts
|
||
|
||
1. **Structure du projet** - Organisation claire avec `app/`, `components/`, `lib/`
|
||
2. **TypeScript** - Utilisation de TypeScript avec configuration stricte
|
||
3. **Séparation des responsabilités** - Services séparés (`lib/services/`)
|
||
4. **Validation d'environnement** - Script `validate-env.ts` présent
|
||
5. **Logger centralisé** - `lib/logger.ts` avec niveaux de log
|
||
6. **Gestion d'erreurs API** - Certaines routes ont une bonne gestion d'erreurs
|
||
7. **Health check** - Endpoint `/api/health` pour monitoring
|
||
|
||
### 🚨 Problèmes Détectés
|
||
|
||
#### 1. **ESLint et TypeScript désactivés en build** 🚨 CRITIQUE
|
||
```javascript
|
||
// next.config.mjs ligne 3-8
|
||
eslint: {
|
||
ignoreDuringBuilds: true, // ❌ DANGEREUX
|
||
},
|
||
typescript: {
|
||
ignoreBuildErrors: true, // ❌ DANGEREUX
|
||
},
|
||
```
|
||
**Impact :**
|
||
- Erreurs TypeScript ignorées
|
||
- Erreurs ESLint ignorées
|
||
- Risque de bugs en production
|
||
|
||
#### 2. **Pas de tests** 🚨 CRITIQUE
|
||
- Aucun fichier `.test.ts` ou `.spec.ts` trouvé
|
||
- Pas de tests unitaires
|
||
- Pas de tests e2e
|
||
- Pas de coverage
|
||
|
||
#### 3. **Pas de metadata SEO** ⚠️ IMPORTANT
|
||
- Metadata manquante dans la plupart des pages
|
||
- Pas de sitemap
|
||
- Pas de robots.txt
|
||
- Pas d'Open Graph complet
|
||
|
||
#### 4. **Gestion d'erreurs incomplète** ⚠️ IMPORTANT
|
||
- Pas de `error.tsx` au niveau des routes
|
||
- Pas de `not-found.tsx` global
|
||
- Gestion d'erreurs incohérente dans les API routes
|
||
|
||
#### 5. **Accessibilité (a11y) non vérifiée** ⚠️ MOYEN
|
||
- Pas de vérification d'accessibilité visible
|
||
- Pas d'attributs ARIA
|
||
- Contraste des couleurs non vérifié
|
||
|
||
#### 6. **Console.log en production** ⚠️ MOYEN
|
||
- Beaucoup de `console.log` dans le code
|
||
- Logger configuré mais pas utilisé partout
|
||
- Risque de fuite d'informations
|
||
|
||
#### 7. **Pas de rate limiting** ⚠️ MOYEN
|
||
- Pas de protection contre les abus
|
||
- API routes exposées sans limitation
|
||
|
||
### 📋 Recommandations Actionnables
|
||
|
||
#### 🔴 CRITIQUE - Réactiver ESLint et TypeScript
|
||
|
||
```typescript
|
||
// next.config.mjs
|
||
const nextConfig = {
|
||
eslint: {
|
||
ignoreDuringBuilds: false, // ✅ Réactiver
|
||
dirs: ['app', 'components', 'lib'], // Limiter aux dossiers pertinents
|
||
},
|
||
typescript: {
|
||
ignoreBuildErrors: false, // ✅ Réactiver
|
||
},
|
||
};
|
||
```
|
||
|
||
**Action immédiate :** Corriger toutes les erreurs TypeScript et ESLint avant de réactiver.
|
||
|
||
#### 🔴 CRITIQUE - Ajouter des tests
|
||
|
||
```bash
|
||
# Installer les dépendances de test
|
||
npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom @types/jest
|
||
```
|
||
|
||
```json
|
||
// package.json
|
||
{
|
||
"scripts": {
|
||
"test": "jest",
|
||
"test:watch": "jest --watch",
|
||
"test:coverage": "jest --coverage"
|
||
}
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// jest.config.js
|
||
const nextJest = require('next/jest')
|
||
|
||
const createJestConfig = nextJest({
|
||
dir: './',
|
||
})
|
||
|
||
const customJestConfig = {
|
||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||
testEnvironment: 'jest-environment-jsdom',
|
||
moduleNameMapper: {
|
||
'^@/(.*)$': '<rootDir>/$1',
|
||
},
|
||
}
|
||
|
||
module.exports = createJestConfig(customJestConfig)
|
||
```
|
||
|
||
Exemple de test :
|
||
```typescript
|
||
// __tests__/components/quote-card.test.tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { QuoteCard } from '@/components/quote-card';
|
||
|
||
describe('QuoteCard', () => {
|
||
it('renders without crashing', () => {
|
||
render(<QuoteCard />);
|
||
expect(screen.getByRole('article')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Créer sitemap et robots.txt
|
||
|
||
```typescript
|
||
// app/sitemap.ts
|
||
import { MetadataRoute } from 'next';
|
||
|
||
export default function sitemap(): MetadataRoute.Sitemap {
|
||
const baseUrl = process.env.NEXTAUTH_URL || 'https://example.com';
|
||
|
||
return [
|
||
{
|
||
url: baseUrl,
|
||
lastModified: new Date(),
|
||
changeFrequency: 'daily',
|
||
priority: 1,
|
||
},
|
||
{
|
||
url: `${baseUrl}/missions`,
|
||
lastModified: new Date(),
|
||
changeFrequency: 'weekly',
|
||
priority: 0.8,
|
||
},
|
||
// ... autres routes
|
||
];
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// app/robots.ts
|
||
import { MetadataRoute } from 'next';
|
||
|
||
export default function robots(): MetadataRoute.Robots {
|
||
return {
|
||
rules: {
|
||
userAgent: '*',
|
||
allow: '/',
|
||
disallow: ['/api/', '/signin', '/signout'],
|
||
},
|
||
sitemap: `${process.env.NEXTAUTH_URL}/sitemap.xml`,
|
||
};
|
||
}
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Créer not-found.tsx
|
||
|
||
```typescript
|
||
// app/not-found.tsx
|
||
import Link from 'next/link';
|
||
|
||
export default function NotFound() {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||
<h2 className="text-4xl font-bold mb-4">404</h2>
|
||
<p className="text-xl mb-8">Page non trouvée</p>
|
||
<Link
|
||
href="/"
|
||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||
>
|
||
Retour à l'accueil
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 🟡 MOYEN - Ajouter rate limiting
|
||
|
||
```bash
|
||
npm install next-rate-limit
|
||
```
|
||
|
||
```typescript
|
||
// lib/rate-limit.ts
|
||
import { rateLimit } from 'next-rate-limit';
|
||
|
||
export const limiter = rateLimit({
|
||
interval: 60 * 1000, // 1 minute
|
||
uniqueTokenPerInterval: 500,
|
||
});
|
||
```
|
||
|
||
```typescript
|
||
// app/api/example/route.ts
|
||
import { limiter } from '@/lib/rate-limit';
|
||
|
||
export async function GET(request: Request) {
|
||
await limiter.check(10, 'CACHE_TOKEN'); // 10 requêtes par minute
|
||
// ... reste du code
|
||
}
|
||
```
|
||
|
||
#### 🟢 MOYEN - Améliorer l'accessibilité
|
||
|
||
```typescript
|
||
// Exemple : Ajouter des attributs ARIA
|
||
<button
|
||
aria-label="Rafraîchir les emails"
|
||
aria-busy={isLoading}
|
||
onClick={handleRefresh}
|
||
>
|
||
<RefreshIcon />
|
||
</button>
|
||
```
|
||
|
||
Installer un linter d'accessibilité :
|
||
```bash
|
||
npm install --save-dev eslint-plugin-jsx-a11y
|
||
```
|
||
|
||
### 📊 Niveau de Maturité
|
||
|
||
**Niveau : Intermédiaire** ⚠️
|
||
|
||
**Justification :**
|
||
- ✅ Structure de projet solide
|
||
- ✅ TypeScript bien utilisé
|
||
- ❌ Pas de tests
|
||
- ❌ ESLint/TypeScript désactivés
|
||
- ⚠️ Gestion d'erreurs partielle
|
||
- ⚠️ SEO incomplet
|
||
|
||
---
|
||
|
||
## 3️⃣ CONFORMITÉ RGPD (GDPR)
|
||
|
||
### 🚨 Problèmes Détectés
|
||
|
||
#### 1. **Stockage de données personnelles sans consentement** 🚨 CRITIQUE
|
||
|
||
**localStorage/sessionStorage utilisé massivement :**
|
||
- 72 occurrences dans le code
|
||
- Stockage d'IDs utilisateur, tokens, données de session
|
||
- Pas de consentement explicite
|
||
- Pas de politique de rétention
|
||
|
||
**Exemples problématiques :**
|
||
```typescript
|
||
// hooks/use-task-notifications.ts
|
||
localStorage.setItem('notified-task-ids', JSON.stringify(ids));
|
||
|
||
// hooks/use-calendar-event-notifications.ts
|
||
localStorage.setItem('notified-event-ids', JSON.stringify(ids));
|
||
|
||
// lib/cache-utils.ts
|
||
localStorage.setItem(fullKey, JSON.stringify(entry));
|
||
|
||
// app/page.tsx
|
||
sessionStorage.setItem('just_logged_out', 'true');
|
||
```
|
||
|
||
#### 2. **Pas de politique de confidentialité visible** 🚨 CRITIQUE
|
||
- Aucune page `/privacy` ou `/confidentialite` trouvée
|
||
- Pas de mention légale
|
||
- Pas de politique de cookies
|
||
|
||
#### 3. **Pas de gestion des droits utilisateurs** 🚨 CRITIQUE
|
||
- Pas d'endpoint pour accès aux données (Article 15)
|
||
- Pas d'endpoint pour rectification (Article 16)
|
||
- Pas d'endpoint pour suppression (Article 17 - droit à l'oubli)
|
||
- Pas d'export des données (Article 20)
|
||
|
||
#### 4. **Cookies sans consentement** ⚠️ IMPORTANT
|
||
- Cookies Next-Auth utilisés (légitimes pour l'authentification)
|
||
- Mais pas de banner de consentement pour les cookies non essentiels
|
||
- Pas de distinction cookies essentiels / non essentiels
|
||
|
||
#### 5. **Données partagées avec des tiers** ⚠️ IMPORTANT
|
||
- Keycloak (authentification)
|
||
- RocketChat (messages)
|
||
- Microsoft Graph (emails, calendrier)
|
||
- Leantime (tâches)
|
||
- Pas de mention explicite dans une politique de confidentialité
|
||
|
||
#### 6. **Logs contenant potentiellement des données personnelles** ⚠️ MOYEN
|
||
```typescript
|
||
// lib/logger.ts
|
||
// Pas de sanitization des données avant logging
|
||
logger.debug('User data', { userId, email }); // Risque RGPD
|
||
```
|
||
|
||
#### 7. **Pas de minimisation des données** ⚠️ MOYEN
|
||
- Stockage de données non nécessaires
|
||
- Pas de nettoyage automatique des données obsolètes
|
||
|
||
### 📋 Recommandations Actionnables
|
||
|
||
#### 🔴 CRITIQUE - Créer une politique de confidentialité
|
||
|
||
```typescript
|
||
// app/confidentialite/page.tsx
|
||
import { Metadata } from 'next';
|
||
|
||
export const metadata: Metadata = {
|
||
title: 'Politique de Confidentialité',
|
||
};
|
||
|
||
export default function ConfidentialitePage() {
|
||
return (
|
||
<div className="container mx-auto p-8 max-w-4xl">
|
||
<h1 className="text-3xl font-bold mb-6">Politique de Confidentialité</h1>
|
||
|
||
<section className="mb-8">
|
||
<h2 className="text-2xl font-semibold mb-4">1. Collecte des données</h2>
|
||
<p>Nous collectons les données suivantes :</p>
|
||
<ul className="list-disc ml-6">
|
||
<li>Données d'authentification (via Keycloak)</li>
|
||
<li>Emails et calendriers (via Microsoft Graph)</li>
|
||
<li>Messages (via RocketChat)</li>
|
||
<li>Tâches (via Leantime)</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section className="mb-8">
|
||
<h2 className="text-2xl font-semibold mb-4">2. Base légale</h2>
|
||
<p>Le traitement est basé sur :</p>
|
||
<ul className="list-disc ml-6">
|
||
<li>Votre consentement</li>
|
||
<li>L'exécution d'un contrat</li>
|
||
<li>L'intérêt légitime</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section className="mb-8">
|
||
<h2 className="text-2xl font-semibold mb-4">3. Vos droits</h2>
|
||
<p>Conformément au RGPD, vous disposez des droits suivants :</p>
|
||
<ul className="list-disc ml-6">
|
||
<li>Droit d'accès (Article 15)</li>
|
||
<li>Droit de rectification (Article 16)</li>
|
||
<li>Droit à l'effacement (Article 17)</li>
|
||
<li>Droit à la portabilité (Article 20)</li>
|
||
<li>Droit d'opposition (Article 21)</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section className="mb-8">
|
||
<h2 className="text-2xl font-semibold mb-4">4. Contact</h2>
|
||
<p>Pour exercer vos droits, contactez : privacy@example.com</p>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 🔴 CRITIQUE - Implémenter les droits utilisateurs
|
||
|
||
```typescript
|
||
// app/api/gdpr/access/route.ts
|
||
import { NextResponse } from 'next/server';
|
||
import { getServerSession } from 'next-auth';
|
||
import { authOptions } from '@/app/api/auth/options';
|
||
import { prisma } from '@/lib/prisma';
|
||
|
||
export async function GET() {
|
||
const session = await getServerSession(authOptions);
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||
}
|
||
|
||
// Collecter toutes les données de l'utilisateur
|
||
const userData = {
|
||
profile: await prisma.user.findUnique({
|
||
where: { id: session.user.id },
|
||
}),
|
||
emails: await prisma.email.findMany({
|
||
where: { userId: session.user.id },
|
||
}),
|
||
calendars: await prisma.calendar.findMany({
|
||
where: { userId: session.user.id },
|
||
}),
|
||
// ... autres données
|
||
};
|
||
|
||
return NextResponse.json(userData);
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// app/api/gdpr/delete/route.ts
|
||
export async function DELETE() {
|
||
const session = await getServerSession(authOptions);
|
||
if (!session?.user?.id) {
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||
}
|
||
|
||
// Supprimer toutes les données de l'utilisateur
|
||
await prisma.user.delete({
|
||
where: { id: session.user.id },
|
||
});
|
||
|
||
// Nettoyer localStorage côté client
|
||
// (nécessite une action côté client)
|
||
|
||
return NextResponse.json({ success: true });
|
||
}
|
||
```
|
||
|
||
#### 🔴 CRITIQUE - Ajouter un banner de consentement cookies
|
||
|
||
```typescript
|
||
// components/cookie-consent.tsx
|
||
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { Button } from '@/components/ui/button';
|
||
|
||
export function CookieConsent() {
|
||
const [showBanner, setShowBanner] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const consent = localStorage.getItem('cookie-consent');
|
||
if (!consent) {
|
||
setShowBanner(true);
|
||
}
|
||
}, []);
|
||
|
||
const acceptCookies = () => {
|
||
localStorage.setItem('cookie-consent', 'accepted');
|
||
localStorage.setItem('cookie-consent-date', new Date().toISOString());
|
||
setShowBanner(false);
|
||
};
|
||
|
||
const rejectCookies = () => {
|
||
localStorage.setItem('cookie-consent', 'rejected');
|
||
// Supprimer les cookies non essentiels
|
||
// ...
|
||
setShowBanner(false);
|
||
};
|
||
|
||
if (!showBanner) return null;
|
||
|
||
return (
|
||
<div className="fixed bottom-0 left-0 right-0 bg-gray-900 text-white p-4 z-50">
|
||
<div className="container mx-auto flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm">
|
||
Nous utilisons des cookies pour améliorer votre expérience.
|
||
<a href="/confidentialite" className="underline ml-1">
|
||
En savoir plus
|
||
</a>
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button onClick={rejectCookies} variant="outline">
|
||
Refuser
|
||
</Button>
|
||
<Button onClick={acceptCookies}>
|
||
Accepter
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Minimiser le stockage localStorage
|
||
|
||
```typescript
|
||
// lib/storage-utils.ts
|
||
export class GDPRCompliantStorage {
|
||
private static getConsent(): boolean {
|
||
return localStorage.getItem('cookie-consent') === 'accepted';
|
||
}
|
||
|
||
static setItem(key: string, value: string): void {
|
||
if (!this.getConsent()) {
|
||
console.warn('Storage blocked: no consent');
|
||
return;
|
||
}
|
||
|
||
// Ajouter une date d'expiration
|
||
const entry = {
|
||
value,
|
||
expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 jours
|
||
};
|
||
|
||
localStorage.setItem(key, JSON.stringify(entry));
|
||
}
|
||
|
||
static getItem(key: string): string | null {
|
||
if (!this.getConsent()) {
|
||
return null;
|
||
}
|
||
|
||
const item = localStorage.getItem(key);
|
||
if (!item) return null;
|
||
|
||
try {
|
||
const entry = JSON.parse(item);
|
||
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||
localStorage.removeItem(key);
|
||
return null;
|
||
}
|
||
return entry.value;
|
||
} catch {
|
||
return item; // Fallback pour compatibilité
|
||
}
|
||
}
|
||
|
||
static clearUserData(userId: string): void {
|
||
// Nettoyer toutes les données d'un utilisateur
|
||
const keys = Object.keys(localStorage);
|
||
keys.forEach(key => {
|
||
if (key.includes(userId) || key.startsWith('notified-')) {
|
||
localStorage.removeItem(key);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Sanitizer les logs
|
||
|
||
```typescript
|
||
// lib/logger.ts - Amélioration
|
||
export const logger = {
|
||
// ... existing code ...
|
||
|
||
sanitize(data: any): any {
|
||
if (!data || typeof data !== 'object') return data;
|
||
|
||
const sensitive = ['password', 'token', 'secret', 'key', 'email'];
|
||
const sanitized = { ...data };
|
||
|
||
for (const key in sanitized) {
|
||
if (sensitive.some(s => key.toLowerCase().includes(s))) {
|
||
sanitized[key] = '[REDACTED]';
|
||
}
|
||
}
|
||
|
||
return sanitized;
|
||
},
|
||
|
||
debug(...args: any[]) {
|
||
if (!isProd) {
|
||
console.debug(...args.map(arg =>
|
||
typeof arg === 'object' ? this.sanitize(arg) : arg
|
||
));
|
||
}
|
||
},
|
||
};
|
||
```
|
||
|
||
### 📊 Niveau de Conformité
|
||
|
||
**Niveau : Faible** 🚫
|
||
|
||
**Justification :**
|
||
- ❌ Pas de politique de confidentialité
|
||
- ❌ Pas de gestion des droits utilisateurs
|
||
- ❌ Stockage localStorage sans consentement
|
||
- ❌ Pas de banner cookies
|
||
- ⚠️ Données partagées avec tiers non documentées
|
||
- ⚠️ Logs non sanitizés
|
||
|
||
---
|
||
|
||
## 4️⃣ SÉCURITÉ
|
||
|
||
### ✅ Points Forts
|
||
|
||
1. **Next-Auth** - Utilisation de Next-Auth avec Keycloak
|
||
2. **Cookies sécurisés** - Configuration correcte des cookies (httpOnly, sameSite)
|
||
3. **DOMPurify** - Utilisé pour sanitizer le HTML (emails)
|
||
4. **Validation d'environnement** - Script de validation des variables d'environnement
|
||
5. **Health check** - Endpoint de monitoring
|
||
|
||
### 🚨 Problèmes Détectés
|
||
|
||
#### 1. **Headers de sécurité incomplets** 🚨 CRITIQUE
|
||
|
||
```javascript
|
||
// next.config.mjs ligne 20-31
|
||
async headers() {
|
||
return [
|
||
{
|
||
source: '/:path*',
|
||
headers: [
|
||
{
|
||
key: 'Content-Security-Policy',
|
||
value: "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net"
|
||
// ❌ CSP incomplet, manque beaucoup de directives
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Manque :**
|
||
- X-Frame-Options
|
||
- X-Content-Type-Options
|
||
- X-XSS-Protection
|
||
- Referrer-Policy
|
||
- Permissions-Policy
|
||
- Strict-Transport-Security (HSTS)
|
||
|
||
#### 2. **CSP trop permissif** 🚨 CRITIQUE
|
||
- CSP ne couvre que `frame-ancestors`
|
||
- Pas de protection contre XSS
|
||
- Pas de restriction des sources de scripts/styles
|
||
|
||
#### 3. **Variables d'environnement exposées** ⚠️ IMPORTANT
|
||
```typescript
|
||
// Beaucoup d'utilisation de process.env.NEXT_PUBLIC_*
|
||
// Ces variables sont exposées au client
|
||
```
|
||
- Risque si des secrets sont dans `NEXT_PUBLIC_*`
|
||
|
||
#### 4. **Pas de rate limiting** ⚠️ IMPORTANT
|
||
- API routes non protégées contre les abus
|
||
- Risque de DDoS
|
||
- Risque de brute force
|
||
|
||
#### 5. **Gestion d'erreurs trop verbeuse** ⚠️ MOYEN
|
||
```typescript
|
||
// Certaines routes retournent des détails d'erreur
|
||
return NextResponse.json({
|
||
error: "Failed",
|
||
details: errorMessage, // ❌ Peut exposer des infos sensibles
|
||
});
|
||
```
|
||
|
||
#### 6. **Pas de CSRF protection explicite** ⚠️ MOYEN
|
||
- Next-Auth gère le CSRF pour l'auth
|
||
- Mais pas de protection explicite pour les autres endpoints
|
||
|
||
#### 7. **Script inline dans le layout** ⚠️ MOYEN
|
||
```typescript
|
||
// app/layout.tsx ligne 29-36
|
||
<script dangerouslySetInnerHTML={{
|
||
__html: `...` // ❌ Script inline
|
||
}} />
|
||
```
|
||
|
||
### 📋 Recommandations Actionnables
|
||
|
||
#### 🔴 CRITIQUE - Améliorer les headers de sécurité
|
||
|
||
```typescript
|
||
// next.config.mjs
|
||
async headers() {
|
||
return [
|
||
{
|
||
source: '/:path*',
|
||
headers: [
|
||
{
|
||
key: 'X-DNS-Prefetch-Control',
|
||
value: 'on'
|
||
},
|
||
{
|
||
key: 'Strict-Transport-Security',
|
||
value: 'max-age=63072000; includeSubDomains; preload'
|
||
},
|
||
{
|
||
key: 'X-Frame-Options',
|
||
value: 'SAMEORIGIN'
|
||
},
|
||
{
|
||
key: 'X-Content-Type-Options',
|
||
value: 'nosniff'
|
||
},
|
||
{
|
||
key: 'X-XSS-Protection',
|
||
value: '1; mode=block'
|
||
},
|
||
{
|
||
key: 'Referrer-Policy',
|
||
value: 'origin-when-cross-origin'
|
||
},
|
||
{
|
||
key: 'Permissions-Policy',
|
||
value: 'camera=(), microphone=(), geolocation=()'
|
||
},
|
||
{
|
||
key: 'Content-Security-Policy',
|
||
value: [
|
||
"default-src 'self'",
|
||
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // ⚠️ À restreindre davantage
|
||
"style-src 'self' 'unsafe-inline'",
|
||
"img-src 'self' data: https:",
|
||
"font-src 'self' data:",
|
||
"connect-src 'self' https://*.slm-lab.net https://*.microsoft.com",
|
||
"frame-src 'self' https://*.slm-lab.net",
|
||
"frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net",
|
||
].join('; ')
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 🟡 IMPORTANT - Ajouter rate limiting
|
||
|
||
Voir section 2, recommandation "Ajouter rate limiting"
|
||
|
||
#### 🟡 IMPORTANT - Sanitizer les erreurs API
|
||
|
||
```typescript
|
||
// lib/api-error-handler.ts
|
||
export function handleApiError(error: unknown) {
|
||
const isDev = process.env.NODE_ENV === 'development';
|
||
|
||
if (error instanceof Error) {
|
||
return {
|
||
error: 'Internal server error',
|
||
message: isDev ? error.message : undefined,
|
||
stack: isDev ? error.stack : undefined,
|
||
};
|
||
}
|
||
|
||
return {
|
||
error: 'Internal server error',
|
||
};
|
||
}
|
||
```
|
||
|
||
#### 🟡 MOYEN - Retirer le script inline
|
||
|
||
```typescript
|
||
// app/layout.tsx
|
||
// Supprimer le script inline et utiliser un composant dédié
|
||
|
||
// components/security-script.tsx
|
||
'use client';
|
||
|
||
import { useEffect } from 'react';
|
||
|
||
export function SecurityScript() {
|
||
useEffect(() => {
|
||
if (typeof window !== 'undefined' && typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
|
||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function() {};
|
||
}
|
||
}, []);
|
||
|
||
return null;
|
||
}
|
||
```
|
||
|
||
### 📊 Score Sécurité
|
||
|
||
**Score : 50/100** ⚠️
|
||
|
||
**Justification :**
|
||
- ✅ Authentification solide (Next-Auth + Keycloak)
|
||
- ✅ Cookies sécurisés
|
||
- ❌ Headers de sécurité incomplets
|
||
- ❌ CSP insuffisant
|
||
- ⚠️ Pas de rate limiting
|
||
- ⚠️ Gestion d'erreurs verbeuse
|
||
|
||
---
|
||
|
||
## 5️⃣ PLAN D'AMÉLIORATION PRIORISÉ
|
||
|
||
### 🔴 CRITIQUE (Bloquant Production)
|
||
|
||
1. **Réactiver ESLint et TypeScript**
|
||
- Complexité : Faible
|
||
- Temps estimé : 2-4h
|
||
- Action : Corriger toutes les erreurs, puis réactiver dans `next.config.mjs`
|
||
|
||
2. **Créer politique de confidentialité et gestion RGPD**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 1-2 jours
|
||
- Action :
|
||
- Créer `/confidentialite`
|
||
- Implémenter endpoints `/api/gdpr/*`
|
||
- Ajouter banner de consentement cookies
|
||
|
||
3. **Améliorer headers de sécurité**
|
||
- Complexité : Faible
|
||
- Temps estimé : 1-2h
|
||
- Action : Compléter la configuration dans `next.config.mjs`
|
||
|
||
4. **Activer l'optimisation d'images**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 4-8h
|
||
- Action : Remplacer tous les `<img>` par `<Image>`
|
||
|
||
5. **Ajouter error.tsx et not-found.tsx**
|
||
- Complexité : Faible
|
||
- Temps estimé : 1-2h
|
||
- Action : Créer les fichiers de gestion d'erreurs
|
||
|
||
### 🟡 IMPORTANT (Recommandé avant Production)
|
||
|
||
6. **Ajouter des tests**
|
||
- Complexité : Élevée
|
||
- Temps estimé : 3-5 jours
|
||
- Action : Setup Jest, écrire tests pour composants critiques
|
||
|
||
7. **Créer sitemap et robots.txt**
|
||
- Complexité : Faible
|
||
- Temps estimé : 1h
|
||
- Action : Créer `app/sitemap.ts` et `app/robots.ts`
|
||
|
||
8. **Ajouter rate limiting**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 2-4h
|
||
- Action : Implémenter sur toutes les API routes
|
||
|
||
9. **Convertir page principale en Server Component**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 4-6h
|
||
- Action : Refactoriser `app/page.tsx`
|
||
|
||
10. **Ajouter metadata SEO**
|
||
- Complexité : Faible
|
||
- Temps estimé : 2-3h
|
||
- Action : Ajouter metadata dans layout et pages principales
|
||
|
||
### 🟢 NICE-TO-HAVE (Amélioration continue)
|
||
|
||
11. **Optimiser bundle size**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 1-2 jours
|
||
- Action : Analyser et optimiser les imports
|
||
|
||
12. **Améliorer accessibilité**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 2-3 jours
|
||
- Action : Audit a11y et corrections
|
||
|
||
13. **Améliorer logging et observabilité**
|
||
- Complexité : Moyenne
|
||
- Temps estimé : 2-3 jours
|
||
- Action : Intégrer Sentry ou équivalent
|
||
|
||
14. **Documentation API**
|
||
- Complexité : Faible
|
||
- Temps estimé : 1-2 jours
|
||
- Action : Documenter les endpoints API
|
||
|
||
---
|
||
|
||
## 📈 SCORES DÉTAILLÉS
|
||
|
||
### Performance : 55/100
|
||
|
||
| Critère | Score | Commentaire |
|
||
|---------|-------|-------------|
|
||
| Core Web Vitals | 40/100 | Pas de mesure, images non optimisées |
|
||
| Server Components | 50/100 | Page principale en Client Component |
|
||
| Image Optimization | 20/100 | Désactivée dans config |
|
||
| Font Optimization | 80/100 | ✅ Utilisation correcte de next/font |
|
||
| Caching | 70/100 | ✅ Redis implémenté |
|
||
| Bundle Size | 50/100 | Non optimisé, pas d'analyse |
|
||
|
||
### Qualité du Code : 60/100
|
||
|
||
| Critère | Score | Commentaire |
|
||
|---------|-------|-------------|
|
||
| Structure | 80/100 | ✅ Bien organisé |
|
||
| TypeScript | 70/100 | ⚠️ Désactivé en build |
|
||
| Tests | 0/100 | ❌ Aucun test |
|
||
| Gestion erreurs | 50/100 | ⚠️ Partielle |
|
||
| SEO | 30/100 | ❌ Metadata manquante |
|
||
| Accessibilité | 40/100 | ⚠️ Non vérifiée |
|
||
|
||
### Sécurité : 50/100
|
||
|
||
| Critère | Score | Commentaire |
|
||
|---------|-------|-------------|
|
||
| Authentification | 80/100 | ✅ Next-Auth + Keycloak |
|
||
| Headers sécurité | 30/100 | ❌ Incomplets |
|
||
| CSP | 40/100 | ❌ Trop permissif |
|
||
| Rate Limiting | 0/100 | ❌ Absent |
|
||
| Gestion erreurs | 50/100 | ⚠️ Trop verbeuse |
|
||
|
||
### RGPD : 40/100
|
||
|
||
| Critère | Score | Commentaire |
|
||
|---------|-------|-------------|
|
||
| Politique confidentialité | 0/100 | ❌ Absente |
|
||
| Consentement cookies | 0/100 | ❌ Absent |
|
||
| Droits utilisateurs | 0/100 | ❌ Non implémentés |
|
||
| Minimisation données | 50/100 | ⚠️ Partielle |
|
||
| Sécurité données | 60/100 | ⚠️ À améliorer |
|
||
|
||
---
|
||
|
||
## 🎯 CONCLUSION
|
||
|
||
Le projet présente une **base solide** avec une architecture Next.js moderne et une structure de code organisée. Cependant, plusieurs **points critiques** doivent être adressés avant la mise en production :
|
||
|
||
1. **Sécurité** : Headers incomplets, pas de rate limiting
|
||
2. **RGPD** : Non conforme, pas de politique de confidentialité
|
||
3. **Performance** : Images non optimisées, page principale en Client Component
|
||
4. **Qualité** : ESLint/TypeScript désactivés, pas de tests
|
||
|
||
**Recommandation :** Traiter au minimum les points **CRITIQUE** avant toute mise en production. Les points **IMPORTANT** devraient être traités dans les 2-4 semaines suivant le déploiement.
|
||
|
||
---
|
||
|
||
**Audit réalisé par :** Expert Next.js Senior
|
||
**Date :** $(date)
|
||
**Version du projet :** 0.1.0
|