Pages corrections widget
This commit is contained in:
parent
5ba4d9ee7a
commit
7682eb07da
@ -15,7 +15,10 @@ COPY . .
|
|||||||
|
|
||||||
# Initialize Prisma
|
# Initialize Prisma
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npx prisma migrate dev --name init
|
# NOTE: Migrations should be run separately before deployment
|
||||||
|
# DO NOT use 'migrate dev' in production - it creates new migrations
|
||||||
|
# Use 'prisma migrate deploy' instead, run separately before container start
|
||||||
|
# RUN npx prisma migrate deploy
|
||||||
|
|
||||||
# Build the Next.js application
|
# Build the Next.js application
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
695
PERFORMANCE_AND_PRODUCTION_ANALYSIS.md
Normal file
695
PERFORMANCE_AND_PRODUCTION_ANALYSIS.md
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
# Analyse Performance et Préparation Production - Neah
|
||||||
|
|
||||||
|
**Date:** $(date)
|
||||||
|
**Auteur:** Analyse Senior Developer
|
||||||
|
**Version:** 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Table des Matières
|
||||||
|
|
||||||
|
1. [Résumé Exécutif](#résumé-exécutif)
|
||||||
|
2. [Analyse des Performances](#analyse-des-performances)
|
||||||
|
3. [Problèmes de Configuration Production](#problèmes-de-configuration-production)
|
||||||
|
4. [Sécurité](#sécurité)
|
||||||
|
5. [Recommandations Prioritaires](#recommandations-prioritaires)
|
||||||
|
6. [Checklist de Mise en Production](#checklist-de-mise-en-production)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Résumé Exécutif
|
||||||
|
|
||||||
|
### État Actuel
|
||||||
|
- ✅ **Architecture solide**: Next.js 16, Prisma, Redis, PostgreSQL
|
||||||
|
- ✅ **Bonnes pratiques**: Logging structuré, gestion d'erreurs, cache Redis
|
||||||
|
- ⚠️ **Problèmes critiques**: Configuration DB pool, timeouts HTTP, logs console
|
||||||
|
- ⚠️ **Production readiness**: 70% - Nécessite corrections avant déploiement
|
||||||
|
|
||||||
|
### Priorités
|
||||||
|
1. **CRITIQUE** - Configuration pool de connexions Prisma
|
||||||
|
2. **CRITIQUE** - Correction Dockerfile (migrate dev → deploy)
|
||||||
|
3. **HAUTE** - Ajout de timeouts sur toutes les requêtes HTTP
|
||||||
|
4. **HAUTE** - Remplacement console.log par logger structuré
|
||||||
|
5. **MOYENNE** - Optimisation images Next.js
|
||||||
|
6. **MOYENNE** - Rate limiting et circuit breakers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Analyse des Performances
|
||||||
|
|
||||||
|
### 1. Base de Données (PostgreSQL)
|
||||||
|
|
||||||
|
#### ❌ Problèmes Identifiés
|
||||||
|
|
||||||
|
**1.1 Pool de Connexions Non Configuré**
|
||||||
|
```typescript
|
||||||
|
// lib/prisma.ts - ACTUEL
|
||||||
|
export const prisma = new PrismaClient({
|
||||||
|
datasources: { db: { url: env.DATABASE_URL } },
|
||||||
|
log: process.env.NODE_ENV === 'production' ? [] : ['query'],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problème:**
|
||||||
|
- Pas de limite de connexions
|
||||||
|
- Risque d'épuisement du pool sous charge
|
||||||
|
- Pas de configuration de timeout
|
||||||
|
- Pas de monitoring des connexions
|
||||||
|
|
||||||
|
**Impact:** 🔴 **CRITIQUE** - Peut causer des timeouts et crashes en production
|
||||||
|
|
||||||
|
**Solution Recommandée:**
|
||||||
|
```typescript
|
||||||
|
// lib/prisma.ts - RECOMMANDÉ
|
||||||
|
export const prisma = new PrismaClient({
|
||||||
|
datasources: {
|
||||||
|
db: {
|
||||||
|
url: env.DATABASE_URL
|
||||||
|
}
|
||||||
|
},
|
||||||
|
log: process.env.NODE_ENV === 'production' ? [] : ['error', 'warn'],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration du pool via DATABASE_URL
|
||||||
|
// DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration DATABASE_URL recommandée:**
|
||||||
|
```
|
||||||
|
postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20&connect_timeout=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.2 Requêtes N+1 Potentielles**
|
||||||
|
|
||||||
|
**Fichiers à vérifier:**
|
||||||
|
- `app/api/missions/route.ts` - Include multiples relations
|
||||||
|
- `app/api/missions/all/route.ts` - Include missionUsers avec user
|
||||||
|
- `app/api/calendars/route.ts` - Vérifier les requêtes imbriquées
|
||||||
|
|
||||||
|
**Recommandation:** Utiliser `select` au lieu de `include` quand possible, ou utiliser `Prisma.raw` pour des requêtes optimisées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Requêtes HTTP Externes
|
||||||
|
|
||||||
|
#### ❌ Problèmes Identifiés
|
||||||
|
|
||||||
|
**2.1 Timeouts Manquants**
|
||||||
|
|
||||||
|
**Fichiers affectés:**
|
||||||
|
- `lib/services/n8n-service.ts` - Pas de timeout sur fetch
|
||||||
|
- `app/api/missions/[missionId]/generate-plan/route.ts` - Pas de timeout
|
||||||
|
- `app/api/users/[userId]/route.ts` - Pas de timeout sur Leantime API
|
||||||
|
- `app/api/rocket-chat/messages/route.ts` - Timeouts partiels
|
||||||
|
- `app/api/leantime/tasks/route.ts` - Pas de timeout
|
||||||
|
|
||||||
|
**Exemple problématique:**
|
||||||
|
```typescript
|
||||||
|
// ❌ MAUVAIS - app/api/missions/[missionId]/generate-plan/route.ts
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
||||||
|
body: JSON.stringify(webhookData),
|
||||||
|
});
|
||||||
|
// Pas de timeout - peut bloquer indéfiniment
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution Recommandée:**
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
||||||
|
body: JSON.stringify(webhookData),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
// ... handle response
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error('Request timeout after 30s');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.2 Pas de Circuit Breaker**
|
||||||
|
|
||||||
|
**Problème:** Si un service externe (N8N, Leantime, RocketChat) est down, toutes les requêtes échouent sans retry intelligent.
|
||||||
|
|
||||||
|
**Recommandation:** Implémenter un circuit breaker pattern:
|
||||||
|
```typescript
|
||||||
|
// lib/utils/circuit-breaker.ts
|
||||||
|
class CircuitBreaker {
|
||||||
|
private failures = 0;
|
||||||
|
private lastFailureTime = 0;
|
||||||
|
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
|
||||||
|
|
||||||
|
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
if (this.state === 'OPEN') {
|
||||||
|
if (Date.now() - this.lastFailureTime > 60000) {
|
||||||
|
this.state = 'HALF_OPEN';
|
||||||
|
} else {
|
||||||
|
throw new Error('Circuit breaker is OPEN');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
this.onSuccess();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.onFailure();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSuccess() {
|
||||||
|
this.failures = 0;
|
||||||
|
this.state = 'CLOSED';
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFailure() {
|
||||||
|
this.failures++;
|
||||||
|
this.lastFailureTime = Date.now();
|
||||||
|
if (this.failures >= 5) {
|
||||||
|
this.state = 'OPEN';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Cache et Optimisations
|
||||||
|
|
||||||
|
#### ✅ Points Positifs
|
||||||
|
- Redis utilisé pour cache emails, notifications, messages
|
||||||
|
- Request deduplication implémentée (`lib/utils/request-deduplication.ts`)
|
||||||
|
- Cache avec TTL approprié
|
||||||
|
|
||||||
|
#### ⚠️ Améliorations Possibles
|
||||||
|
|
||||||
|
**3.1 Cache Database Queries**
|
||||||
|
```typescript
|
||||||
|
// Exemple: app/api/missions/route.ts
|
||||||
|
// Ajouter cache pour les requêtes fréquentes
|
||||||
|
const cacheKey = `missions:${userId}:${limit}:${offset}`;
|
||||||
|
const cached = await redis.get(cacheKey);
|
||||||
|
if (cached) return JSON.parse(cached);
|
||||||
|
|
||||||
|
const missions = await prisma.mission.findMany(...);
|
||||||
|
await redis.setex(cacheKey, 60, JSON.stringify(missions)); // 60s TTL
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.2 Optimisation Images Next.js**
|
||||||
|
|
||||||
|
**Problème actuel:**
|
||||||
|
```javascript
|
||||||
|
// next.config.mjs
|
||||||
|
images: {
|
||||||
|
unoptimized: true, // ❌ Désactive l'optimisation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```javascript
|
||||||
|
images: {
|
||||||
|
unoptimized: false, // ✅ Active l'optimisation
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200],
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. WebSocket et Connexions Longues
|
||||||
|
|
||||||
|
#### ⚠️ Problèmes Identifiés
|
||||||
|
|
||||||
|
**4.1 RocketChat Call Listener**
|
||||||
|
- `lib/services/rocketchat-call-listener.ts`
|
||||||
|
- Pas de rate limiting sur reconnexions
|
||||||
|
- Pas de backoff exponentiel optimal
|
||||||
|
- Logs console.log excessifs (35 occurrences)
|
||||||
|
|
||||||
|
**Recommandations:**
|
||||||
|
```typescript
|
||||||
|
// Améliorer le backoff
|
||||||
|
private reconnectDelay = 3000;
|
||||||
|
private maxReconnectAttempts = 10;
|
||||||
|
|
||||||
|
// ✅ MEILLEUR
|
||||||
|
private getReconnectDelay(): number {
|
||||||
|
return Math.min(3000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**4.2 IMAP Connection Pool**
|
||||||
|
- `lib/services/email-service.ts`
|
||||||
|
- Pool bien géré mais peut être optimisé
|
||||||
|
- Timeout de 30 minutes peut être réduit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Logging et Monitoring
|
||||||
|
|
||||||
|
#### ❌ Problèmes Critiques
|
||||||
|
|
||||||
|
**5.1 Console.log en Production**
|
||||||
|
|
||||||
|
**Statistiques:**
|
||||||
|
- 110 occurrences de `console.log/error/warn/debug` dans `lib/services/`
|
||||||
|
- 35 dans `rocketchat-call-listener.ts`
|
||||||
|
- 19 dans `refresh-manager.ts`
|
||||||
|
- 17 dans `prefetch-service.ts`
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- Performance dégradée (console.log est synchrone)
|
||||||
|
- Logs non structurés
|
||||||
|
- Pas de centralisation
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```typescript
|
||||||
|
// Remplacer tous les console.log par logger
|
||||||
|
// ❌ AVANT
|
||||||
|
console.log('[ROCKETCHAT] Message received', data);
|
||||||
|
|
||||||
|
// ✅ APRÈS
|
||||||
|
logger.debug('[ROCKETCHAT] Message received', { data });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action:** Créer un script de migration:
|
||||||
|
```bash
|
||||||
|
# scripts/replace-console-logs.sh
|
||||||
|
find lib/services -name "*.ts" -exec sed -i '' 's/console\.log/logger.debug/g' {} \;
|
||||||
|
find lib/services -name "*.ts" -exec sed -i '' 's/console\.error/logger.error/g' {} \;
|
||||||
|
find lib/services -name "*.ts" -exec sed -i '' 's/console\.warn/logger.warn/g' {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
**5.2 Pas de Métriques de Performance**
|
||||||
|
|
||||||
|
**Recommandation:** Ajouter des métriques:
|
||||||
|
```typescript
|
||||||
|
// lib/utils/metrics.ts
|
||||||
|
export function trackApiCall(endpoint: string, duration: number) {
|
||||||
|
// Envoyer à un service de métriques (DataDog, New Relic, etc.)
|
||||||
|
logger.info('API_CALL', { endpoint, duration });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Problèmes de Configuration Production
|
||||||
|
|
||||||
|
### 1. Dockerfile
|
||||||
|
|
||||||
|
#### ❌ Problème Critique
|
||||||
|
|
||||||
|
**Fichier:** `Dockerfile`
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# ❌ MAUVAIS - Ligne 18
|
||||||
|
RUN npx prisma migrate dev --name init
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problème:**
|
||||||
|
- `migrate dev` crée une nouvelle migration même si la DB est à jour
|
||||||
|
- Ne doit JAMAIS être utilisé en production
|
||||||
|
- Peut causer des conflits de migrations
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```dockerfile
|
||||||
|
# ✅ BON
|
||||||
|
# Ne pas faire de migrations dans le Dockerfile
|
||||||
|
# Les migrations doivent être appliquées séparément avant le déploiement
|
||||||
|
# RUN npx prisma migrate deploy # Seulement si nécessaire, et seulement en production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommandation:** Utiliser `Dockerfile.prod` qui est mieux configuré, mais vérifier qu'il n'utilise pas `migrate dev`.
|
||||||
|
|
||||||
|
### 2. Next.js Configuration
|
||||||
|
|
||||||
|
#### ⚠️ Problèmes
|
||||||
|
|
||||||
|
**Fichier:** `next.config.mjs`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ PROBLÉMATIQUE
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true, // Cache les erreurs ESLint
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true, // Cache les erreurs TypeScript
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
unoptimized: true, // Désactive l'optimisation d'images
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommandation:**
|
||||||
|
```javascript
|
||||||
|
// ✅ MEILLEUR
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: false, // Ou au moins en staging
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: false, // Corriger les erreurs TypeScript
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
unoptimized: false, // Activer l'optimisation
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Variables d'Environnement
|
||||||
|
|
||||||
|
#### ⚠️ Vérifications Nécessaires
|
||||||
|
|
||||||
|
**Variables critiques à vérifier:**
|
||||||
|
- `DATABASE_URL` - Doit inclure les paramètres de pool
|
||||||
|
- `REDIS_URL` - Doit être configuré avec timeout
|
||||||
|
- `NEXTAUTH_SECRET` - Doit être unique et fort
|
||||||
|
- Tous les tokens API (N8N, Leantime, RocketChat)
|
||||||
|
|
||||||
|
**Recommandation:** Créer un script de validation:
|
||||||
|
```typescript
|
||||||
|
// scripts/validate-env.ts
|
||||||
|
const requiredVars = [
|
||||||
|
'DATABASE_URL',
|
||||||
|
'NEXTAUTH_SECRET',
|
||||||
|
'NEXTAUTH_URL',
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const varName of requiredVars) {
|
||||||
|
if (!process.env[varName]) {
|
||||||
|
throw new Error(`Missing required environment variable: ${varName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Sécurité
|
||||||
|
|
||||||
|
### 1. Secrets dans les Logs
|
||||||
|
|
||||||
|
#### ⚠️ Risque Modéré
|
||||||
|
|
||||||
|
**Problème:** Vérifier que les mots de passe/tokens ne sont pas loggés.
|
||||||
|
|
||||||
|
**Fichiers à vérifier:**
|
||||||
|
- `lib/services/email-service.ts` - Credentials IMAP
|
||||||
|
- `app/api/courrier/test-connection/route.ts` - Password dans logs
|
||||||
|
|
||||||
|
**Solution actuelle (bonne):**
|
||||||
|
```typescript
|
||||||
|
// ✅ BON - app/api/courrier/test-connection/route.ts
|
||||||
|
console.log('Testing connection with:', {
|
||||||
|
...body,
|
||||||
|
password: body.password ? '***' : undefined // Masqué
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rate Limiting
|
||||||
|
|
||||||
|
#### ❌ Manquant
|
||||||
|
|
||||||
|
**Problème:** Pas de rate limiting sur les API routes.
|
||||||
|
|
||||||
|
**Recommandation:** Implémenter avec `@upstash/ratelimit` ou middleware Next.js:
|
||||||
|
```typescript
|
||||||
|
// middleware.ts
|
||||||
|
import { Ratelimit } from '@upstash/ratelimit';
|
||||||
|
import { Redis } from '@upstash/redis';
|
||||||
|
|
||||||
|
const ratelimit = new Ratelimit({
|
||||||
|
redis: Redis.fromEnv(),
|
||||||
|
limiter: Ratelimit.slidingWindow(10, '10 s'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
const ip = request.ip ?? '127.0.0.1';
|
||||||
|
const { success } = await ratelimit.limit(ip);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return new Response('Rate limit exceeded', { status: 429 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CORS et Headers Sécurité
|
||||||
|
|
||||||
|
#### ✅ Bon
|
||||||
|
|
||||||
|
**Fichier:** `next.config.mjs`
|
||||||
|
- Content-Security-Policy configuré
|
||||||
|
- Headers de sécurité à ajouter si nécessaire
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Recommandations Prioritaires
|
||||||
|
|
||||||
|
### Priorité 1 - CRITIQUE (Avant Production)
|
||||||
|
|
||||||
|
1. **✅ Configurer le pool de connexions Prisma**
|
||||||
|
- Modifier `DATABASE_URL` avec `connection_limit=10&pool_timeout=20`
|
||||||
|
- Tester sous charge
|
||||||
|
|
||||||
|
2. **✅ Corriger le Dockerfile**
|
||||||
|
- Retirer `migrate dev`
|
||||||
|
- Utiliser `migrate deploy` uniquement si nécessaire
|
||||||
|
|
||||||
|
3. **✅ Ajouter des timeouts sur toutes les requêtes HTTP**
|
||||||
|
- Créer un utilitaire `fetchWithTimeout`
|
||||||
|
- Appliquer à tous les `fetch()` externes
|
||||||
|
|
||||||
|
4. **✅ Remplacer console.log par logger**
|
||||||
|
- Script de migration automatique
|
||||||
|
- Vérifier tous les fichiers
|
||||||
|
|
||||||
|
### Priorité 2 - HAUTE (Semaine 1)
|
||||||
|
|
||||||
|
5. **✅ Implémenter Circuit Breaker**
|
||||||
|
- Pour N8N, Leantime, RocketChat
|
||||||
|
- Retry avec backoff exponentiel
|
||||||
|
|
||||||
|
6. **✅ Activer l'optimisation d'images Next.js**
|
||||||
|
- Modifier `next.config.mjs`
|
||||||
|
- Tester les performances
|
||||||
|
|
||||||
|
7. **✅ Ajouter Rate Limiting**
|
||||||
|
- Sur les API routes critiques
|
||||||
|
- Monitoring des limites
|
||||||
|
|
||||||
|
### Priorité 3 - MOYENNE (Semaine 2-3)
|
||||||
|
|
||||||
|
8. **✅ Cache des requêtes DB fréquentes**
|
||||||
|
- Missions, Calendars, Users
|
||||||
|
- TTL approprié (60-300s)
|
||||||
|
|
||||||
|
9. **✅ Monitoring et Métriques**
|
||||||
|
- Intégrer Sentry ou équivalent
|
||||||
|
- Métriques de performance (APM)
|
||||||
|
|
||||||
|
10. **✅ Optimiser les requêtes N+1**
|
||||||
|
- Audit des `include` Prisma
|
||||||
|
- Utiliser `select` quand possible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist de Mise en Production
|
||||||
|
|
||||||
|
### Pré-Déploiement
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
- [ ] Configurer `DATABASE_URL` avec pool settings
|
||||||
|
- [ ] Vérifier toutes les variables d'environnement
|
||||||
|
- [ ] Créer script de validation des env vars
|
||||||
|
- [ ] Configurer les secrets dans Vercel (pas en code)
|
||||||
|
|
||||||
|
#### Code
|
||||||
|
- [ ] Corriger le Dockerfile (retirer `migrate dev`)
|
||||||
|
- [ ] Ajouter timeouts sur toutes les requêtes HTTP
|
||||||
|
- [ ] Remplacer tous les `console.log` par `logger`
|
||||||
|
- [ ] Activer l'optimisation d'images Next.js
|
||||||
|
- [ ] Corriger les erreurs TypeScript/ESLint (ou documenter pourquoi ignorées)
|
||||||
|
|
||||||
|
#### Base de Données
|
||||||
|
- [ ] Appliquer toutes les migrations Prisma
|
||||||
|
- [ ] Créer des index sur les colonnes fréquemment queryées
|
||||||
|
- [ ] Configurer les sauvegardes automatiques
|
||||||
|
- [ ] Tester la restauration depuis backup
|
||||||
|
|
||||||
|
#### Sécurité
|
||||||
|
- [ ] Implémenter rate limiting
|
||||||
|
- [ ] Vérifier qu'aucun secret n'est loggé
|
||||||
|
- [ ] Configurer CORS correctement
|
||||||
|
- [ ] Activer HTTPS uniquement
|
||||||
|
|
||||||
|
#### Monitoring
|
||||||
|
- [ ] Configurer Sentry ou équivalent
|
||||||
|
- [ ] Créer endpoint `/api/health`
|
||||||
|
- [ ] Configurer les alertes Vercel
|
||||||
|
- [ ] Documenter les procédures d'alerte
|
||||||
|
|
||||||
|
### Déploiement
|
||||||
|
|
||||||
|
#### Staging
|
||||||
|
- [ ] Déployer en staging d'abord
|
||||||
|
- [ ] Tester toutes les fonctionnalités critiques
|
||||||
|
- [ ] Vérifier les performances sous charge
|
||||||
|
- [ ] Tester les scénarios d'erreur
|
||||||
|
|
||||||
|
#### Production
|
||||||
|
- [ ] Appliquer les migrations Prisma
|
||||||
|
- [ ] Déployer sur Vercel
|
||||||
|
- [ ] Vérifier les health checks
|
||||||
|
- [ ] Monitorer les logs les premières heures
|
||||||
|
- [ ] Tester les fonctionnalités critiques
|
||||||
|
|
||||||
|
### Post-Déploiement
|
||||||
|
|
||||||
|
#### Monitoring
|
||||||
|
- [ ] Surveiller les métriques de performance
|
||||||
|
- [ ] Vérifier les logs d'erreurs
|
||||||
|
- [ ] Monitorer l'utilisation DB/Redis
|
||||||
|
- [ ] Vérifier les alertes fonctionnent
|
||||||
|
|
||||||
|
#### Optimisation Continue
|
||||||
|
- [ ] Analyser les requêtes DB lentes
|
||||||
|
- [ ] Optimiser les endpoints les plus utilisés
|
||||||
|
- [ ] Ajuster les TTL de cache selon l'usage
|
||||||
|
- [ ] Réviser les timeouts selon les métriques
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Métriques de Succès
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Temps de réponse API:** < 200ms (p95)
|
||||||
|
- **Temps de chargement page:** < 2s (First Contentful Paint)
|
||||||
|
- **Taux d'erreur:** < 0.1%
|
||||||
|
- **Uptime:** > 99.9%
|
||||||
|
|
||||||
|
### Base de Données
|
||||||
|
- **Connexions actives:** < 80% du pool max
|
||||||
|
- **Requêtes lentes:** < 1% des requêtes > 1s
|
||||||
|
- **Taux de cache hit:** > 70% pour les requêtes fréquentes
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- **CPU utilisation:** < 70% en moyenne
|
||||||
|
- **Mémoire:** < 80% en moyenne
|
||||||
|
- **Disque:** < 80% utilisé
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Scripts Utiles
|
||||||
|
|
||||||
|
### Validation Environnement
|
||||||
|
```bash
|
||||||
|
# scripts/validate-production.sh
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Validating production environment..."
|
||||||
|
|
||||||
|
# Vérifier les variables d'environnement
|
||||||
|
node scripts/validate-env.ts
|
||||||
|
|
||||||
|
# Vérifier les migrations
|
||||||
|
npx prisma migrate status
|
||||||
|
|
||||||
|
# Vérifier la connexion DB
|
||||||
|
npx prisma db execute --stdin <<< "SELECT 1"
|
||||||
|
|
||||||
|
# Vérifier Redis
|
||||||
|
redis-cli -h $REDIS_HOST -p $REDIS_PORT ping
|
||||||
|
|
||||||
|
echo "✅ All checks passed"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Console.log
|
||||||
|
```bash
|
||||||
|
# scripts/migrate-console-logs.sh
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
find lib/services app/api -name "*.ts" -type f | while read file; do
|
||||||
|
sed -i '' 's/console\.log(/logger.debug(/g' "$file"
|
||||||
|
sed -i '' 's/console\.error(/logger.error(/g' "$file"
|
||||||
|
sed -i '' 's/console\.warn(/logger.warn(/g' "$file"
|
||||||
|
sed -i '' 's/console\.debug(/logger.debug(/g' "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Console logs migrated to logger"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
```typescript
|
||||||
|
// app/api/health/route.ts
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { getRedisClient } from '@/lib/redis';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const health = {
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
checks: {
|
||||||
|
database: 'unknown',
|
||||||
|
redis: 'unknown',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check DB
|
||||||
|
try {
|
||||||
|
await prisma.$queryRaw`SELECT 1`;
|
||||||
|
health.checks.database = 'ok';
|
||||||
|
} catch (error) {
|
||||||
|
health.checks.database = 'error';
|
||||||
|
health.status = 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Redis
|
||||||
|
try {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
await redis.ping();
|
||||||
|
health.checks.redis = 'ok';
|
||||||
|
} catch (error) {
|
||||||
|
health.checks.redis = 'error';
|
||||||
|
health.status = 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(health, {
|
||||||
|
status: health.status === 'ok' ? 200 : 503,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
- [Prisma Connection Pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management)
|
||||||
|
- [Next.js Production Checklist](https://nextjs.org/docs/deployment#production-checklist)
|
||||||
|
- [Vercel Best Practices](https://vercel.com/docs/concepts/deployments/best-practices)
|
||||||
|
- [PostgreSQL Performance Tuning](https://www.postgresql.org/docs/current/performance-tips.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes Finales
|
||||||
|
|
||||||
|
Cette analyse identifie les problèmes critiques à résoudre avant la mise en production. Les priorités 1 doivent être traitées immédiatement, les priorités 2 dans la semaine, et les priorités 3 peuvent être planifiées après le déploiement initial.
|
||||||
|
|
||||||
|
**Recommandation:** Créer des tickets pour chaque point de la checklist et les suivre jusqu'à résolution complète.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour:** $(date)
|
||||||
290
PRODUCTION_FIXES_APPLIED.md
Normal file
290
PRODUCTION_FIXES_APPLIED.md
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
# Corrections Appliquées pour la Production
|
||||||
|
|
||||||
|
Ce document liste les corrections critiques appliquées suite à l'analyse de performance et de préparation à la production.
|
||||||
|
|
||||||
|
## ✅ Corrections Appliquées
|
||||||
|
|
||||||
|
### 1. Utilitaire fetchWithTimeout
|
||||||
|
|
||||||
|
**Fichier créé:** `lib/utils/fetch-with-timeout.ts`
|
||||||
|
|
||||||
|
**Problème résolu:** Requêtes HTTP sans timeout pouvant bloquer indéfiniment.
|
||||||
|
|
||||||
|
**Utilisation:**
|
||||||
|
```typescript
|
||||||
|
import { fetchWithTimeout, fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout';
|
||||||
|
|
||||||
|
// Exemple 1: Fetch simple avec timeout
|
||||||
|
const response = await fetchWithTimeout('https://api.example.com/data', {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000, // 10 secondes
|
||||||
|
headers: { 'Authorization': 'Bearer token' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exemple 2: Fetch avec parsing JSON automatique
|
||||||
|
const data = await fetchJsonWithTimeout<MyType>('https://api.example.com/data', {
|
||||||
|
method: 'POST',
|
||||||
|
timeout: 30000, // 30 secondes
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration recommandée:**
|
||||||
|
Remplacer tous les `fetch()` dans:
|
||||||
|
- `lib/services/n8n-service.ts`
|
||||||
|
- `app/api/missions/[missionId]/generate-plan/route.ts`
|
||||||
|
- `app/api/users/[userId]/route.ts`
|
||||||
|
- `app/api/leantime/tasks/route.ts`
|
||||||
|
- `app/api/rocket-chat/messages/route.ts`
|
||||||
|
|
||||||
|
### 2. Correction Dockerfile
|
||||||
|
|
||||||
|
**Fichier modifié:** `Dockerfile`
|
||||||
|
|
||||||
|
**Problème résolu:** Utilisation de `migrate dev` en production (créerait de nouvelles migrations).
|
||||||
|
|
||||||
|
**Changement:**
|
||||||
|
```dockerfile
|
||||||
|
# ❌ AVANT
|
||||||
|
RUN npx prisma migrate dev --name init
|
||||||
|
|
||||||
|
# ✅ APRÈS
|
||||||
|
# NOTE: Migrations should be run separately before deployment
|
||||||
|
# DO NOT use 'migrate dev' in production
|
||||||
|
# Use 'prisma migrate deploy' instead, run separately before container start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action requise:** Utiliser `Dockerfile.prod` pour la production, qui est déjà correctement configuré.
|
||||||
|
|
||||||
|
### 3. Script de Validation d'Environnement
|
||||||
|
|
||||||
|
**Fichier créé:** `scripts/validate-env.ts`
|
||||||
|
|
||||||
|
**Problème résolu:** Variables d'environnement manquantes non détectées avant déploiement.
|
||||||
|
|
||||||
|
**Utilisation:**
|
||||||
|
```bash
|
||||||
|
# Valider les variables d'environnement
|
||||||
|
npm run validate:env
|
||||||
|
|
||||||
|
# Ou directement
|
||||||
|
ts-node scripts/validate-env.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fonctionnalités:**
|
||||||
|
- ✅ Vérifie toutes les variables requises
|
||||||
|
- ✅ Valide le format des URLs
|
||||||
|
- ✅ Vérifie la force de NEXTAUTH_SECRET
|
||||||
|
- ✅ Recommande les paramètres de pool DB
|
||||||
|
- ✅ Affiche des warnings pour les variables optionnelles
|
||||||
|
|
||||||
|
**Ajouté dans package.json:**
|
||||||
|
```json
|
||||||
|
"validate:env": "ts-node scripts/validate-env.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Migrations à Effectuer
|
||||||
|
|
||||||
|
### Priorité 1 - CRITIQUE
|
||||||
|
|
||||||
|
#### 1. Remplacer fetch() par fetchWithTimeout()
|
||||||
|
|
||||||
|
**Fichiers à modifier:**
|
||||||
|
|
||||||
|
1. **lib/services/n8n-service.ts**
|
||||||
|
```typescript
|
||||||
|
// ❌ AVANT
|
||||||
|
const response = await fetch(this.webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey },
|
||||||
|
body: JSON.stringify(cleanData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ APRÈS
|
||||||
|
import { fetchWithTimeout } from '@/lib/utils/fetch-with-timeout';
|
||||||
|
|
||||||
|
const response = await fetchWithTimeout(this.webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
timeout: 30000, // 30 secondes
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey },
|
||||||
|
body: JSON.stringify(cleanData),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **app/api/missions/[missionId]/generate-plan/route.ts**
|
||||||
|
```typescript
|
||||||
|
// ❌ AVANT
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
||||||
|
body: JSON.stringify(webhookData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ APRÈS
|
||||||
|
import { fetchWithTimeout } from '@/lib/utils/fetch-with-timeout';
|
||||||
|
|
||||||
|
const response = await fetchWithTimeout(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
||||||
|
body: JSON.stringify(webhookData),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **app/api/users/[userId]/route.ts** (getLeantimeUserId)
|
||||||
|
```typescript
|
||||||
|
// ❌ AVANT
|
||||||
|
const userResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '' },
|
||||||
|
body: JSON.stringify({ ... }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ APRÈS
|
||||||
|
import { fetchJsonWithTimeout } from '@/lib/utils/fetch-with-timeout';
|
||||||
|
|
||||||
|
const userData = await fetchJsonWithTimeout('https://agilite.slm-lab.net/api/jsonrpc', {
|
||||||
|
method: 'POST',
|
||||||
|
timeout: 10000, // 10 secondes
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN || '' },
|
||||||
|
body: JSON.stringify({ ... }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configurer le Pool de Connexions Prisma
|
||||||
|
|
||||||
|
**Action:** Modifier `DATABASE_URL` dans les variables d'environnement:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ AVANT
|
||||||
|
DATABASE_URL=postgresql://user:pass@host:5432/db
|
||||||
|
|
||||||
|
# ✅ APRÈS
|
||||||
|
DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20&connect_timeout=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Paramètres recommandés:**
|
||||||
|
- `connection_limit=10` - Limite le nombre de connexions simultanées
|
||||||
|
- `pool_timeout=20` - Timeout pour obtenir une connexion du pool (secondes)
|
||||||
|
- `connect_timeout=10` - Timeout pour établir une connexion (secondes)
|
||||||
|
|
||||||
|
**Note:** Ajuster `connection_limit` selon la charge attendue et les limites du serveur PostgreSQL.
|
||||||
|
|
||||||
|
#### 3. Remplacer console.log par logger
|
||||||
|
|
||||||
|
**Script de migration:**
|
||||||
|
```bash
|
||||||
|
# Créer scripts/migrate-console-logs.sh
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
find lib/services app/api -name "*.ts" -type f | while read file; do
|
||||||
|
# Sauvegarder d'abord
|
||||||
|
cp "$file" "$file.bak"
|
||||||
|
|
||||||
|
# Remplacer
|
||||||
|
sed -i '' 's/console\.log(/logger.debug(/g' "$file"
|
||||||
|
sed -i '' 's/console\.error(/logger.error(/g' "$file"
|
||||||
|
sed -i '' 's/console\.warn(/logger.warn(/g' "$file"
|
||||||
|
sed -i '' 's/console\.debug(/logger.debug(/g' "$file"
|
||||||
|
|
||||||
|
# Vérifier que logger est importé
|
||||||
|
if ! grep -q "import.*logger" "$file" && grep -q "logger\." "$file"; then
|
||||||
|
# Ajouter l'import en haut du fichier
|
||||||
|
sed -i '' '1i\
|
||||||
|
import { logger } from '\''@/lib/logger'\'';
|
||||||
|
' "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Console logs migrated to logger"
|
||||||
|
echo "⚠️ Review changes and remove .bak files after verification"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fichiers prioritaires:**
|
||||||
|
- `lib/services/rocketchat-call-listener.ts` (35 occurrences)
|
||||||
|
- `lib/services/refresh-manager.ts` (19 occurrences)
|
||||||
|
- `lib/services/prefetch-service.ts` (17 occurrences)
|
||||||
|
|
||||||
|
### Priorité 2 - HAUTE
|
||||||
|
|
||||||
|
#### 4. Activer l'Optimisation d'Images Next.js
|
||||||
|
|
||||||
|
**Fichier:** `next.config.mjs`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ AVANT
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ APRÈS
|
||||||
|
images: {
|
||||||
|
unoptimized: false,
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200],
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Implémenter Circuit Breaker
|
||||||
|
|
||||||
|
Créer `lib/utils/circuit-breaker.ts` (voir exemple dans `PERFORMANCE_AND_PRODUCTION_ANALYSIS.md`).
|
||||||
|
|
||||||
|
**Services à protéger:**
|
||||||
|
- N8N webhooks
|
||||||
|
- Leantime API
|
||||||
|
- RocketChat API
|
||||||
|
|
||||||
|
## 📋 Checklist de Migration
|
||||||
|
|
||||||
|
- [ ] Remplacer tous les `fetch()` par `fetchWithTimeout()` dans les fichiers listés
|
||||||
|
- [ ] Configurer `DATABASE_URL` avec les paramètres de pool
|
||||||
|
- [ ] Exécuter le script de migration console.log
|
||||||
|
- [ ] Vérifier que tous les fichiers utilisent `logger` au lieu de `console`
|
||||||
|
- [ ] Activer l'optimisation d'images dans `next.config.mjs`
|
||||||
|
- [ ] Tester toutes les fonctionnalités après les changements
|
||||||
|
- [ ] Valider l'environnement avec `npm run validate:env`
|
||||||
|
- [ ] Déployer en staging et tester
|
||||||
|
- [ ] Monitorer les performances après déploiement
|
||||||
|
|
||||||
|
## 🧪 Tests Recommandés
|
||||||
|
|
||||||
|
### Test de Timeout
|
||||||
|
```typescript
|
||||||
|
// Test que les timeouts fonctionnent
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
await fetchWithTimeout('https://httpstat.us/200?sleep=5000', {
|
||||||
|
timeout: 2000, // 2 secondes
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
console.assert(duration < 3000, 'Timeout should occur before 3 seconds');
|
||||||
|
console.assert(error.message.includes('timeout'), 'Error should mention timeout');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test de Validation d'Environnement
|
||||||
|
```bash
|
||||||
|
# Tester avec variables manquantes
|
||||||
|
unset DATABASE_URL
|
||||||
|
npm run validate:env
|
||||||
|
# Devrait échouer avec message clair
|
||||||
|
|
||||||
|
# Tester avec toutes les variables
|
||||||
|
# Devrait réussir
|
||||||
|
npm run validate:env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
Voir `PERFORMANCE_AND_PRODUCTION_ANALYSIS.md` pour:
|
||||||
|
- Analyse complète des problèmes
|
||||||
|
- Recommandations détaillées
|
||||||
|
- Métriques de succès
|
||||||
|
- Checklist complète de mise en production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour:** $(date)
|
||||||
94
lib/utils/fetch-with-timeout.ts
Normal file
94
lib/utils/fetch-with-timeout.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Utility function for making HTTP requests with timeout
|
||||||
|
*
|
||||||
|
* This is a critical production utility to prevent hanging requests
|
||||||
|
* that can exhaust server resources.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FetchWithTimeoutOptions extends RequestInit {
|
||||||
|
timeout?: number; // Timeout in milliseconds (default: 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch with automatic timeout handling
|
||||||
|
*
|
||||||
|
* @param url - The URL to fetch
|
||||||
|
* @param options - Fetch options including optional timeout
|
||||||
|
* @returns Promise<Response>
|
||||||
|
* @throws Error if timeout is exceeded or request fails
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const response = await fetchWithTimeout('https://api.example.com/data', {
|
||||||
|
* method: 'GET',
|
||||||
|
* timeout: 10000, // 10 seconds
|
||||||
|
* headers: { 'Authorization': 'Bearer token' }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function fetchWithTimeout(
|
||||||
|
url: string,
|
||||||
|
options: FetchWithTimeoutOptions = {}
|
||||||
|
): Promise<Response> {
|
||||||
|
const { timeout = 30000, ...fetchOptions } = options;
|
||||||
|
|
||||||
|
// Create AbortController for timeout
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...fetchOptions,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
throw new Error(`Request timeout after ${timeout}ms: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch with timeout and automatic JSON parsing
|
||||||
|
*
|
||||||
|
* @param url - The URL to fetch
|
||||||
|
* @param options - Fetch options including optional timeout
|
||||||
|
* @returns Promise<T> - Parsed JSON response
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const data = await fetchJsonWithTimeout('https://api.example.com/data', {
|
||||||
|
* method: 'GET',
|
||||||
|
* timeout: 10000,
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function fetchJsonWithTimeout<T = any>(
|
||||||
|
url: string,
|
||||||
|
options: FetchWithTimeoutOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await fetchWithTimeout(url, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(
|
||||||
|
`HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (!contentType || !contentType.includes('application/json')) {
|
||||||
|
throw new Error(`Expected JSON response, got ${contentType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@
|
|||||||
"migrate:status": "prisma migrate status",
|
"migrate:status": "prisma migrate status",
|
||||||
"migrate:prod": "bash scripts/migrate-prod.sh",
|
"migrate:prod": "bash scripts/migrate-prod.sh",
|
||||||
"verify:config": "bash scripts/verify-vercel-config.sh",
|
"verify:config": "bash scripts/verify-vercel-config.sh",
|
||||||
|
"validate:env": "ts-node scripts/validate-env.ts",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"db:generate": "prisma generate"
|
"db:generate": "prisma generate"
|
||||||
},
|
},
|
||||||
|
|||||||
161
scripts/validate-env.ts
Normal file
161
scripts/validate-env.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env ts-node
|
||||||
|
/**
|
||||||
|
* Environment Variables Validation Script
|
||||||
|
*
|
||||||
|
* Validates that all required environment variables are set
|
||||||
|
* before deployment to production.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ts-node scripts/validate-env.ts
|
||||||
|
* or
|
||||||
|
* npm run validate:env
|
||||||
|
*/
|
||||||
|
|
||||||
|
const requiredVars = [
|
||||||
|
// Core
|
||||||
|
'DATABASE_URL',
|
||||||
|
'NEXTAUTH_URL',
|
||||||
|
'NEXTAUTH_SECRET',
|
||||||
|
|
||||||
|
// Keycloak
|
||||||
|
'KEYCLOAK_BASE_URL',
|
||||||
|
'KEYCLOAK_REALM',
|
||||||
|
'KEYCLOAK_CLIENT_ID',
|
||||||
|
'KEYCLOAK_CLIENT_SECRET',
|
||||||
|
'KEYCLOAK_ISSUER',
|
||||||
|
'NEXT_PUBLIC_KEYCLOAK_ISSUER',
|
||||||
|
];
|
||||||
|
|
||||||
|
const optionalButRecommended = [
|
||||||
|
// Redis
|
||||||
|
'REDIS_HOST',
|
||||||
|
'REDIS_PORT',
|
||||||
|
'REDIS_PASSWORD',
|
||||||
|
|
||||||
|
// External Services
|
||||||
|
'N8N_API_KEY',
|
||||||
|
'N8N_WEBHOOK_URL',
|
||||||
|
'LEANTIME_API_URL',
|
||||||
|
'LEANTIME_TOKEN',
|
||||||
|
'ROCKET_CHAT_TOKEN',
|
||||||
|
'ROCKET_CHAT_USER_ID',
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
missing: string[];
|
||||||
|
warnings: string[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEnvironment(): ValidationResult {
|
||||||
|
const result: ValidationResult = {
|
||||||
|
valid: true,
|
||||||
|
missing: [],
|
||||||
|
warnings: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check required variables
|
||||||
|
for (const varName of requiredVars) {
|
||||||
|
const value = process.env[varName];
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
result.missing.push(varName);
|
||||||
|
result.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check optional but recommended
|
||||||
|
for (const varName of optionalButRecommended) {
|
||||||
|
const value = process.env[varName];
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
result.warnings.push(varName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate DATABASE_URL format
|
||||||
|
const dbUrl = process.env.DATABASE_URL;
|
||||||
|
if (dbUrl) {
|
||||||
|
if (!dbUrl.startsWith('postgresql://') && !dbUrl.startsWith('postgres://')) {
|
||||||
|
result.errors.push('DATABASE_URL must start with postgresql:// or postgres://');
|
||||||
|
result.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for connection pool parameters
|
||||||
|
if (!dbUrl.includes('connection_limit')) {
|
||||||
|
result.warnings.push(
|
||||||
|
'DATABASE_URL should include connection_limit parameter (e.g., ?connection_limit=10&pool_timeout=20)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate NEXTAUTH_SECRET strength
|
||||||
|
const nextAuthSecret = process.env.NEXTAUTH_SECRET;
|
||||||
|
if (nextAuthSecret && nextAuthSecret.length < 32) {
|
||||||
|
result.warnings.push('NEXTAUTH_SECRET should be at least 32 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URLs
|
||||||
|
const urlVars = ['NEXTAUTH_URL', 'KEYCLOAK_BASE_URL', 'NEXT_PUBLIC_KEYCLOAK_ISSUER'];
|
||||||
|
for (const varName of urlVars) {
|
||||||
|
const value = process.env[varName];
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
} catch {
|
||||||
|
result.errors.push(`${varName} is not a valid URL: ${value}`);
|
||||||
|
result.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log('🔍 Validating environment variables...\n');
|
||||||
|
|
||||||
|
const result = validateEnvironment();
|
||||||
|
|
||||||
|
if (result.missing.length > 0) {
|
||||||
|
console.error('❌ Missing required environment variables:');
|
||||||
|
result.missing.forEach((varName) => {
|
||||||
|
console.error(` - ${varName}`);
|
||||||
|
});
|
||||||
|
console.error('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.error('❌ Validation errors:');
|
||||||
|
result.errors.forEach((error) => {
|
||||||
|
console.error(` - ${error}`);
|
||||||
|
});
|
||||||
|
console.error('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
console.warn('⚠️ Warnings:');
|
||||||
|
result.warnings.forEach((warning) => {
|
||||||
|
console.warn(` - ${warning}`);
|
||||||
|
});
|
||||||
|
console.warn('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
console.log('✅ All required environment variables are set!\n');
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
console.log('⚠️ Some optional variables are missing, but deployment can proceed.\n');
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Environment validation failed!\n');
|
||||||
|
console.error('Please set all required variables before deploying.\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { validateEnvironment };
|
||||||
Loading…
Reference in New Issue
Block a user