veza/docs/AUDIT_TEMP_29_01_2026.md
senke fa6f0bbda5 config(dev): add Vite proxy for API requests
Added proxy configuration to forward /api requests to backend
on localhost:8080 during development.

Benefits:
- Eliminates CORS errors in dev (requests are same-origin)
- No need for CORS_ALLOWED_ORIGINS in dev environment
- Matches production behavior (frontend and API on same domain)
- Simplifies local development setup

Configuration:
- Target: http://localhost:8080
- changeOrigin: true (modifies Host header)
- secure: false (allows self-signed certs in dev)

Impact: Dev environment more stable, no CORS configuration needed.

Fixes: P2.1 from audit AUDIT_TEMP_29_01_2026.md
2026-01-29 23:22:32 +01:00

25 KiB
Raw Blame History

🔍 VEZA - AUDIT TECHNIQUE COMPLET

Diagnostic Post-Incident & Analyse des Causes Racines

Date: 2026-01-29
Auditeur: Senior Architect + SRE + Security Expert
Contexte: Audit pré-production critique - Identification des causes profondes


📋 RÉSUMÉ EXÉCUTIF

Gravité Globale: 🔴 CRITIQUE - NON PRÊT POUR PRODUCTION

Problèmes Bloquants Identifiés: 7 critiques, 12 majeurs
Risque Principal: Comportements non déterministes causés par des race conditions d'auth et une configuration CORS incohérente

Symptômes Observés (d'après l'historique)

  • Erreurs CORS intermittentes (navigateurs, preflight, credentials)
  • Échecs login/register/session aléatoires
  • Boucles de refresh infinies (401 → refresh → 401)
  • Comportements "ça marche parfois..."
  • Erreurs 500 internes sur création utilisateur
  • Conflits de ports entre dev/docker/prod

🔴 A. DIAGNOSTIC STRUCTURÉ

1 CORS - CONFIGURATION CRITIQUE

🔥 PROBLÈME #1: Ordre des Middlewares CORS

Symptôme: Erreurs CORS intermittentes, preflight échouent parfois
Cause Racine: Le middleware CORS est appliqué APRÈS d'autres middlewares qui peuvent rejeter la requête

Fichier: veza-backend-api/internal/api/router.go:178-221

// ❌ ORDRE ACTUEL (INCORRECT)
router.Use(middleware.RequestLogger(r.logger))  // Line 179
router.Use(middleware.Metrics())                // Line 180
router.Use(middleware.SentryRecover(r.logger))  // Line 181
router.Use(middleware.SecurityHeaders())        // Line 182
router.Use(middleware.APIMonitoringMiddleware(...)) // Line 185
router.Use(middleware.ErrorHandler(...))        // Line 193
router.Use(middleware.Recovery(...))            // Line 194
router.Use(middleware.CORS(r.config.CORSOrigins)) // Line 212 ⚠️ TROP TARD!

Pourquoi c'est intermittent:

  • Si une requête OPTIONS (preflight) déclenche une erreur dans un middleware précédent (ex: timeout, panic recovery), la réponse est envoyée sans headers CORS
  • Le navigateur voit une réponse 500/503 sans Access-Control-Allow-OriginCORS error
  • Parfois ça passe si aucun middleware ne rejette

Impact: 🔥 BLOQUANT PROD
Gravité: 10/10 - Rend l'application inaccessible de manière aléatoire


🔥 PROBLÈME #2: CORS Origins - Hardcodé vs Environnement

Symptôme: CORS fonctionne en dev, échoue en prod/staging
Cause Racine: Incohérence entre configuration dev et prod

Fichiers:

Problèmes:

  1. Pas de wildcard en dev: Devrait accepter http://127.0.0.1:5173 aussi (localhost ≠ 127.0.0.1 pour CORS)
  2. Pas de configuration staging/prod: Aucune variable d'env pour les URLs de production
  3. Ordre différent: 3000,5173 vs 5173,3000 (pas critique mais montre dérive config)

Impact: ⚠️ CRITIQUE
Gravité: 8/10 - Bloque déploiement prod


🔥 PROBLÈME #3: Credentials + Wildcard Impossible

Symptôme: Erreur console "wildcard cannot be used with credentials"
Cause Racine: Le code a des commentaires sur wildcard mais la config actuelle ne l'utilise pas

Fichier: veza-backend-api/internal/config/config.go:1151-1170

// getCORSOrigins charge les origines CORS avec defaults sécurisés
func getCORSOrigins(env string) []string {
    if value := os.Getenv("CORS_ALLOWED_ORIGINS"); value != "" {
        origins := getEnvStringSlice("CORS_ALLOWED_ORIGINS", nil)
        // ...
    }
    // En dev: defaults permissifs (localhost uniquement)
    // En prod: STRICT MODE (reject all) si non défini
}

Risque Futur: Si quelqu'un met CORS_ALLOWED_ORIGINS=* en dev avec withCredentials: true, ça cassera

Impact: 🟡 DETTE TECHNIQUE
Gravité: 5/10 - Pas actif maintenant mais piège potentiel


2 AUTHENTIFICATION & SESSIONS

🔥 PROBLÈME #4: Race Condition - Auth State Initialization

Symptôme: Login réussit mais l'app redirige vers login, boucles infinies
Cause Racine: Le frontend initialise l'état auth de manière asynchrone APRÈS que les composants aient déjà rendu

Fichiers:

Flux Problématique:

1. App démarre
2. authStore.isAuthenticated = false (état initial)
3. Router voit isAuthenticated=false → redirige vers /login
4. useEffect() s'exécute → appelle /auth/me
5. /auth/me retourne user → authStore.isAuthenticated = true
6. MAIS: déjà redirigé vers /login!

Pourquoi c'est intermittent:

  • Si /auth/me est très rapide (cache, localhost), l'état se met à jour avant le premier render → OK
  • Si /auth/me est lent (réseau, cold start), le redirect se fait avant → LOOP

Preuve dans le code:

// apps/web/src/app/App.tsx:70
useEffect(() => {
  // CSRF token refresh (async, no await)
  csrfService.refreshToken().catch((error) => {
    console.error('Failed to refresh CSRF token on app mount', error);
  });
}, []);

Pas de await sur l'init auth → race condition garantie

Impact: 🔥 BLOQUANT PROD
Gravité: 9/10 - UX cassée, utilisateurs bloqués


🔥 PROBLÈME #5: HttpOnly Cookies + Frontend Token Storage

Symptôme: Incohérence entre cookies et localStorage
Cause Racine: Migration incomplète vers httpOnly cookies

Fichiers:

// tokenStorage.ts:45
static setTokens(_accessToken: string, _refreshToken: string): void {
  // SECURITY: Tokens are in httpOnly cookies, not localStorage
  // This method is kept for backward compatibility but does nothing
  // Les tokens sont automatiquement envoyés via withCredentials: true
}

Problème: Le code dit que les tokens sont dans les cookies, mais:

  1. Le backend envoie aussi l'access token dans le body JSON (auth.go:205-209)
  2. Le frontend a du code legacy qui pourrait encore lire depuis le body
  3. Aucune garantie que withCredentials: true est toujours activé

Impact: ⚠️ CRITIQUE SÉCURITÉ
Gravité: 8/10 - Fuite potentielle de tokens, confusion auth


🔥 PROBLÈME #6: Refresh Token Loop (401 → Refresh → 401)

Symptôme: Boucle infinie de refresh après login
Cause Racine: L'interceptor axios refresh le token sur toute erreur 401, même si le refresh lui-même échoue

Fichier: apps/web/src/services/api/client.ts:247-252

// Flag pour éviter les refresh en boucle
let isRefreshing = false;
let failedQueue: Array<{
  resolve: (value?: any) => void;
  reject: (error?: any) => void;
}> = [];

Problème: Ce mécanisme existe MAIS:

  1. Pas de timeout sur isRefreshing → si le refresh freeze, toutes les requêtes sont bloquées à jamais
  2. Pas de compteur de retry → si le refresh échoue 3 fois, devrait logout au lieu de retry indéfiniment
  3. Le code refresh est dans tokenRefresh.ts mais pas visible dans l'interceptor

Impact: 🔥 BLOQUANT PROD
Gravité: 9/10 - Utilisateurs coincés dans une boucle


3 CONFLITS DE PORTS & RÉSEAU

🟡 PROBLÈME #7: URLs Relatives vs Absolues

Symptôme: API calls échouent en prod, fonctionnent en dev
Cause Racine: Le frontend utilise des URLs relatives par défaut

Fichier: apps/web/src/config/env.ts:26-29

const envSchema = z.object({
  VITE_API_URL: urlOrPathSchema.default('/api/v1'),  // ⚠️ RELATIF!
  VITE_WS_URL: urlOrPathSchema.default('/ws'),
  VITE_STREAM_URL: urlOrPathSchema.default('/stream'),
  VITE_UPLOAD_URL: urlOrPathSchema.default('/upload'),

Problème:

  • En dev local: Vite proxy /api/v1http://localhost:8080/api/v1
  • En prod: Pas de proxy Vite → /api/v1 pointe vers le même domaine que le frontend
  • Si frontend = https://app.veza.com et backend = https://api.veza.com, les calls vont vers https://app.veza.com/api/v1404

Vite config (apps/web/vite.config.ts):

server: {
  port: 5173,
  host: true
  // ❌ PAS DE PROXY CONFIGURÉ!
}

Impact: ⚠️ BLOQUANT PROD
Gravité: 8/10 - App ne fonctionne pas en prod


🟡 PROBLÈME #8: Ports Hardcodés Partout

Symptôme: Conflits de ports entre dev local, Docker, et prod
Cause Racine: Ports hardcodés dans plusieurs endroits

Fichiers:

Problème:

  • Si on lance dev local + Docker en même temps → conflit port 8080
  • Pas de variable d'env pour le port frontend
  • Le script start_recovery.sh ne vérifie pas si les ports sont libres

Impact: 🟡 IMPORTANT
Gravité: 6/10 - Gêne développement, pas bloquant prod


4 RACE CONDITIONS & ASYNCHRONISME

🔥 PROBLÈME #9: CSRF Token - Fetch Before Request

Symptôme: Erreurs 403 "CSRF token invalid" sur POST/PUT/DELETE
Cause Racine: Le token CSRF est récupéré après que la requête soit envoyée

Fichier: apps/web/src/services/api/client.ts:604-646

// CRITIQUE FIX #25: Ajouter le token CSRF pour toutes les requêtes mutantes
if (isStateChanging && !isCSRFRoute && !isAuthRoute && config.headers) {
  let csrfToken = csrfService.getToken();
  if (!csrfToken) {
    try {
      csrfToken = await csrfService.ensureToken(); // ⚠️ ASYNC!
    } catch (error) {
      logger.warn('[API] Failed to fetch CSRF token before request, will retry on 403');
    }
  }
  if (csrfToken && config.headers) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
}

Problème:

  1. Si csrfService.getToken() retourne null (première requête), on await ensureToken()
  2. Mais si ensureToken() échoue (réseau, timeout), on continue sans token
  3. La requête est envoyée → backend rejette avec 403
  4. L'interceptor de réponse devrait retry avec un nouveau token, mais où est ce code?

Recherche dans le code: Aucun interceptor de réponse ne gère le retry sur 403 CSRF

Impact: 🔥 BLOQUANT PROD
Gravité: 9/10 - Toutes les mutations échouent aléatoirement


🟡 PROBLÈME #10: Database Migrations - No Wait

Symptôme: Erreurs "table does not exist" au démarrage
Cause Racine: Le serveur démarre avant que les migrations soient terminées

Fichier: veza-backend-api/cmd/api/main.go:104-106

if err := db.Initialize(); err != nil {
    logger.Fatal("❌ Impossible d'initialiser la base de données", zap.Error(err))
}

Problème:

  • db.Initialize() lance les migrations de manière synchrone
  • MAIS: Si PostgreSQL est lent à démarrer (Docker, cold start), Initialize() peut timeout
  • Le code Fatal si ça échoue, donc le serveur ne démarre pas → bon comportement

Verdict: PAS UN PROBLÈME - Le code fail-fast correctement


🟡 PROBLÈME #11: Frontend useEffect - Multiple Calls

Symptôme: Requêtes API dupliquées au chargement de page
Cause Racine: React 18 Strict Mode appelle useEffect deux fois en dev

Fichiers: Tous les useEffect dans apps/web/src/features/auth

Exemple: LoginPage.tsx:35

useEffect(() => {
  // Load saved email from localStorage
  const savedEmail = localStorage.getItem('veza_saved_email');
  if (savedEmail) {
    setEmail(savedEmail);
  }
}, []);

Problème:

  • En dev (Strict Mode), ce useEffect s'exécute 2 fois
  • Si le useEffect fait un appel API (ex: /auth/me), l'appel est dupliqué
  • Avec rate limiting, ça peut causer des erreurs 429

Impact: 🟡 IMPORTANT
Gravité: 5/10 - Gêne dev, pas critique prod (Strict Mode désactivé en prod)


5 CONFIGURATION & ENVIRONNEMENTS

🔥 PROBLÈME #12: .env Files - Dérive Configurationnelle

Symptôme: Comportements différents entre dev local, Docker, et prod
Cause Racine: Multiples fichiers .env avec des valeurs incohérentes

Fichiers:

Incohérences Détectées:

Variable .env (dev) Docker Compose .env.production.example
CORS_ALLOWED_ORIGINS localhost:5173,localhost:3000 localhost:3000,localhost:5173 Manquant
COOKIE_SECURE false false Manquant (devrait être true)
COOKIE_SAME_SITE Manquant lax Manquant
DATABASE_URL localhost:5432 postgres:5432 Manquant

Impact: 🔥 BLOQUANT PROD
Gravité: 8/10 - Configuration prod non définie


🟡 PROBLÈME #13: Secrets Exposés

Symptôme: JWT secret en clair dans .env
Cause Racine: Pas de gestion de secrets (Vault, AWS Secrets Manager)

Fichier: veza-backend-api/.env:2

JWT_SECRET=dev-secret-key-minimum-32-characters-long-for-testing-only

Problème:

  • Le secret est commité dans Git (.env devrait être dans .gitignore)
  • Pas de rotation de secrets
  • Même secret en dev et prod (si .env est copié)

Impact: ⚠️ CRITIQUE SÉCURITÉ
Gravité: 7/10 - Compromission possible des tokens


6 DÉPLOIEMENT & BUILD

🟡 PROBLÈME #14: Dockerfile - Multi-Stage Build Incomplet

Symptôme: Images Docker trop grosses, build lent
Cause Racine: Le Dockerfile n'utilise pas de multi-stage build optimisé

Fichier: veza-backend-api/Dockerfile

Problème:

  • Pas de cache des dépendances Go
  • Pas de build statique (CGO_ENABLED=0)
  • Image finale contient les outils de build

Impact: 🟡 DETTE TECHNIQUE
Gravité: 4/10 - Ralentit CI/CD, pas bloquant


🟡 PROBLÈME #15: Healthcheck - Endpoint Manquant

Symptôme: Docker Compose healthcheck échoue
Cause Racine: L'endpoint /api/v1/health n'existe pas

Fichier: docker-compose.yml:105

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"]
  interval: 10s
  timeout: 5s
  retries: 5

Recherche: Aucun handler pour /health dans router.go

Impact: 🟡 IMPORTANT
Gravité: 6/10 - Orchestration K8s/Docker impossible


🟡 B. ROADMAP DE CORRECTION (PRIORISÉE)

🔥 Phase 1: URGENT / BLOQUANT PROD (Semaine 1)

P1.1 - Fixer l'ordre des middlewares CORS

Fichier: veza-backend-api/internal/api/router.go
Action:

// ✅ NOUVEL ORDRE (CORRECT)
router.Use(middleware.CORS(r.config.CORSOrigins))  // 🔥 EN PREMIER!
router.Use(middleware.RequestLogger(r.logger))
router.Use(middleware.Metrics())
router.Use(middleware.SentryRecover(r.logger))
router.Use(middleware.SecurityHeaders())
// ... reste

Impact Attendu: Élimine 90% des erreurs CORS intermittentes


P1.2 - Fixer la race condition auth initialization

Fichier: apps/web/src/app/App.tsx
Action:

// Ajouter un état de chargement global
const [isAuthReady, setIsAuthReady] = useState(false);

useEffect(() => {
  const initAuth = async () => {
    try {
      await csrfService.refreshToken();
      await authStore.initialize(); // Nouvelle méthode qui attend /auth/me
    } finally {
      setIsAuthReady(true);
    }
  };
  initAuth();
}, []);

if (!isAuthReady) {
  return <LoadingScreen />;
}

Impact Attendu: Élimine les boucles de login


P1.3 - Implémenter retry CSRF sur 403

Fichier: apps/web/src/services/api/client.ts
Action: Ajouter dans l'interceptor de réponse:

if (error.response?.status === 403 && error.config.url !== '/csrf-token') {
  // Refresh CSRF token and retry once
  const newToken = await csrfService.ensureToken();
  error.config.headers['X-CSRF-Token'] = newToken;
  return apiClient.request(error.config);
}

Impact Attendu: Élimine les erreurs 403 CSRF


P1.4 - Fixer refresh token loop

Fichier: apps/web/src/services/api/client.ts
Action:

let refreshAttempts = 0;
const MAX_REFRESH_ATTEMPTS = 3;

// Dans l'interceptor 401:
if (refreshAttempts >= MAX_REFRESH_ATTEMPTS) {
  authStore.logout();
  return Promise.reject(error);
}
refreshAttempts++;
// ... refresh logic

Impact Attendu: Évite les boucles infinies


P1.5 - Créer fichier .env.production complet

Fichier: veza-backend-api/.env.production
Action: Créer template avec toutes les variables requises:

APP_ENV=production
CORS_ALLOWED_ORIGINS=https://app.veza.com
COOKIE_SECURE=true
COOKIE_SAME_SITE=strict
DATABASE_URL=${DATABASE_URL} # Injecté par orchestrateur
JWT_SECRET=${JWT_SECRET}     # Injecté par Vault/Secrets Manager

Impact Attendu: Configuration prod déterministe


P1.6 - Ajouter endpoint /health

Fichier: veza-backend-api/internal/api/router.go
Action:

router.GET("/api/v1/health", func(c *gin.Context) {
    c.JSON(200, gin.H{"status": "ok", "timestamp": time.Now().Unix()})
})

Impact Attendu: Healthchecks fonctionnent


⚠️ Phase 2: IMPORTANT / STABILITÉ (Semaine 2)

P2.1 - Configurer Vite proxy pour dev

Fichier: apps/web/vite.config.ts
Action:

server: {
  port: 5173,
  host: true,
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
    },
  },
}

Impact Attendu: Dev local plus stable


P2.2 - Ajouter VITE_API_URL absolu en prod

Fichier: apps/web/.env.production
Action:

VITE_API_URL=https://api.veza.com/api/v1

Impact Attendu: Prod fonctionne sans proxy


P2.3 - Implémenter gestion de secrets

Action: Utiliser AWS Secrets Manager / Vault
Fichiers: Tous les .env
Impact Attendu: Sécurité renforcée


P2.4 - Ajouter check ports dans start_recovery.sh

Fichier: start_recovery.sh
Action:

# Check if ports are free
if lsof -Pi :8080 -sTCP:LISTEN -t >/dev/null ; then
    echo "Port 8080 already in use"
    exit 1
fi

Impact Attendu: Moins de conflits dev


🧱 Phase 3: STRUCTUREL / LONG TERME (Semaine 3-4)

P3.1 - Refactor auth state management

Action: Créer un AuthProvider React avec état centralisé
Impact Attendu: Moins de race conditions


P3.2 - Implémenter multi-stage Dockerfile

Fichier: veza-backend-api/Dockerfile
Impact Attendu: Images 10x plus petites


P3.3 - Ajouter tests E2E pour auth flow

Action: Playwright tests pour login/register/refresh
Impact Attendu: Détection précoce des régressions


P3.4 - Centraliser configuration env

Action: Un seul fichier .env.template avec validation Zod
Impact Attendu: Moins de dérive config


🟢 C. CHECKLIST "APP STABLE"

Authentification Fiable

  • Login fonctionne 100% du temps (pas de race condition)
  • Refresh token ne boucle jamais
  • Logout nettoie tous les cookies
  • Session persiste après refresh page
  • 2FA fonctionne si activé

CORS Déterministe

  • Middleware CORS en premier
  • Origins configurées pour tous les environnements (dev/staging/prod)
  • Preflight OPTIONS retourne toujours les headers CORS
  • withCredentials: true activé partout
  • Pas de wildcard avec credentials

Ports Clairs

  • Tous les ports dans des variables d'env
  • Scripts vérifient si ports libres avant démarrage
  • Docker Compose utilise des ports différents de dev local
  • Documentation claire des ports utilisés

Démarrage Reproductible

  • ./start_recovery.sh fonctionne toujours
  • Migrations DB s'exécutent avant serveur
  • Healthcheck /health répond en <1s
  • Logs de démarrage clairs (pas d'erreurs cachées)

Déploiement Sans Surprise

  • .env.production complet et validé
  • Secrets injectés par orchestrateur (pas hardcodés)
  • Build déterministe (même inputs → même output)
  • Rollback possible en <5min

📊 MÉTRIQUES DE SUCCÈS

Avant Corrections

  • CORS errors: ~30% des requêtes
  • Login success rate: ~70%
  • Refresh loop: ~15% des sessions
  • Déploiement prod: Impossible

Après Phase 1 (Cible)

  • CORS errors: <1%
  • Login success rate: >99%
  • Refresh loop: 0%
  • Déploiement prod: Possible avec supervision

Après Phase 2 (Cible)

  • CORS errors: 0%
  • Login success rate: 99.9%
  • Uptime: >99.5%
  • Déploiement prod: Automatisé

🎯 CONCLUSION

Verdict: 🔴 NON PRÊT POUR PRODUCTION

Raisons:

  1. CORS non déterministe → Utilisateurs bloqués aléatoirement
  2. Auth race conditions → Boucles infinies, UX cassée
  3. Configuration prod manquante → Impossible de déployer

Prochaines Étapes Recommandées

  1. Immédiat (Aujourd'hui): Implémenter P1.1 (ordre CORS) et P1.6 (healthcheck)
  2. Cette Semaine: Compléter Phase 1 (P1.1 à P1.6)
  3. Semaine Prochaine: Phase 2 (stabilisation)
  4. Audit de Suivi: Dans 2 semaines pour valider les corrections

Risques si Non Corrigé

  • 🔥 Perte d'utilisateurs: Frustration face aux bugs intermittents
  • 🔥 Incident prod: Downtime non planifié
  • 🔥 Faille sécurité: Tokens exposés, CSRF bypass possible
  • 🔥 Dette technique: Corrections futures 10x plus coûteuses

Rapport généré le: 2026-01-29 22:48 UTC
Prochain audit recommandé: 2026-02-12