diff --git a/AUDIT_COMPLET.md b/AUDIT_COMPLET.md new file mode 100644 index 0000000..468b8c3 --- /dev/null +++ b/AUDIT_COMPLET.md @@ -0,0 +1,1217 @@ +# 🔍 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 : `` au lieu de `` +- `app/mission-tab/[missionId]/page.tsx` ligne 222 : `` 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 `` par `` : +```typescript +// Avant +{mission.name} + +// Après +import Image from 'next/image'; +{mission.name} +``` + +#### 🟡 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 ( +
+
+
+ }> + + + }> + + + }> + + + }> + + +
+
+ }> + + + }> + + +
+
+
+ ); +} +``` + +#### 🟡 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 ( +
+
+
+ ); +} +``` + +```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 ( +
+

Une erreur est survenue

+ +
+ ); +} +``` + +#### 🟢 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: ['/jest.setup.js'], + testEnvironment: 'jest-environment-jsdom', + moduleNameMapper: { + '^@/(.*)$': '/$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(); + 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 ( +
+

404

+

Page non trouvée

+ + Retour à l'accueil + +
+ ); +} +``` + +#### 🟡 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 + +``` + +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 ( +
+

Politique de Confidentialité

+ +
+

1. Collecte des données

+

Nous collectons les données suivantes :

+
    +
  • Données d'authentification (via Keycloak)
  • +
  • Emails et calendriers (via Microsoft Graph)
  • +
  • Messages (via RocketChat)
  • +
  • Tâches (via Leantime)
  • +
+
+ +
+

2. Base légale

+

Le traitement est basé sur :

+
    +
  • Votre consentement
  • +
  • L'exécution d'un contrat
  • +
  • L'intérêt légitime
  • +
+
+ +
+

3. Vos droits

+

Conformément au RGPD, vous disposez des droits suivants :

+
    +
  • Droit d'accès (Article 15)
  • +
  • Droit de rectification (Article 16)
  • +
  • Droit à l'effacement (Article 17)
  • +
  • Droit à la portabilité (Article 20)
  • +
  • Droit d'opposition (Article 21)
  • +
+
+ +
+

4. Contact

+

Pour exercer vos droits, contactez : privacy@example.com

+
+
+ ); +} +``` + +#### 🔴 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 ( +
+
+
+

+ Nous utilisons des cookies pour améliorer votre expérience. + + En savoir plus + +

+
+
+ + +
+
+
+ ); +} +``` + +#### 🟡 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 +