From 08596eda71092119620af4c1a971143eb6f86a95 Mon Sep 17 00:00:00 2001 From: senke Date: Fri, 26 Dec 2025 23:11:40 +0100 Subject: [PATCH] =?UTF-8?q?[AUDIT]=20Analyse=20compl=C3=A8te=20de=20l'endp?= =?UTF-8?q?oint=20d'enregistrement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Audit du flux complet Register (handler → service → DB) - Identification de 5 causes probables d'échec - Recommandations d'actions correctives prioritaires - Scripts de diagnostic pour identifier l'erreur réelle --- REGISTER_ENDPOINT_AUDIT.md | 395 +++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 REGISTER_ENDPOINT_AUDIT.md diff --git a/REGISTER_ENDPOINT_AUDIT.md b/REGISTER_ENDPOINT_AUDIT.md new file mode 100644 index 000000000..68191fb21 --- /dev/null +++ b/REGISTER_ENDPOINT_AUDIT.md @@ -0,0 +1,395 @@ +# 🔍 AUDIT COMPLET : Endpoint d'enregistrement `/api/v1/auth/register` + +**Date**: 2025-12-26 +**Status**: ❌ Échec systématique avec code 9000 "Failed to create user" + +--- + +## 📋 FLUX COMPLET D'ENREGISTREMENT + +### 1. Handler (`internal/handlers/auth.go`) + +**Fonction**: `Register(authService, sessionService, logger)` + +**Flux**: +1. ✅ Bind et validation JSON (`BindAndValidateJSON`) +2. ✅ Création d'un contexte avec timeout (5 secondes) +3. ✅ Appel `authService.Register(ctx, email, username, password)` +4. ❌ **ÉCHEC ICI** : Retourne erreur générique code 9000 +5. ⏭️ Création de session (non-bloquant si échoue) +6. ⏭️ Construction de la réponse (jamais atteint) + +**Problème identifié**: +- Ligne 179 : `RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create user", err))` +- L'erreur du service est encapsulée dans une erreur générique +- **L'erreur réelle de la base de données est perdue** + +--- + +### 2. Service Auth (`internal/core/auth/service.go`) + +**Fonction**: `Register(ctx, email, username, password)` + +#### 2.1 Validation Email ✅ +- Utilise `emailValidator.Validate(email)` +- Retourne erreur explicite si invalide + +#### 2.2 Vérification Username Unique ✅ +- Requête: `SELECT COUNT(*) FROM users WHERE LOWER(username) = LOWER(?)` +- Retourne `ErrUserAlreadyExists` si existe + +#### 2.3 Validation Password ✅ +- Utilise `passwordValidator.Validate(password)` +- Retourne erreur explicite si faible + +#### 2.4 Hash Password ✅ +- Utilise `bcrypt.GenerateFromPassword` +- Retourne erreur si échec + +#### 2.5 Génération Slug ⚠️ +```go +baseSlug := strings.ToLower(username) +slug := baseSlug +// Boucle pour trouver un slug unique +``` +- **PROBLÈME POTENTIEL** : Si la requête `COUNT(*)` échoue, retourne erreur générique +- Si `counter > 1000`, utilise timestamp (peu probable d'atteindre cette limite) + +#### 2.6 Création Objet User ✅ +```go +user := &models.User{ + ID: uuid.New(), + Email: email, + Username: username, + Slug: slug, + PasswordHash: string(hashedPassword), + Role: "user", // ⚠️ ENUM PostgreSQL + IsActive: true, + IsVerified: true, + IsBanned: false, + TokenVersion: 0, + LoginCount: 0, + CreatedAt: now, + UpdatedAt: now, +} +``` + +**Champs NOT NULL vérifiés**: +- ✅ `id` (UUID généré) +- ✅ `email` (fourni) +- ✅ `username` (fourni) +- ✅ `slug` (généré) +- ✅ `role` (défini à "user") +- ✅ `is_active` (défini à true) +- ✅ `is_verified` (défini à true) +- ✅ `is_banned` (défini à false) +- ✅ `token_version` (défini à 0) +- ✅ `login_count` (défini à 0) +- ✅ `created_at` (défini explicitement) +- ✅ `updated_at` (défini explicitement) + +#### 2.7 Insertion Base de Données ❌ **POINT DE DÉFAILLANCE** + +```go +result := s.db.WithContext(ctx).Omit("Roles", "TrackLikes").Create(user) +if result.Error != nil { + // Gestion d'erreur... +} +``` + +**Gestion d'erreur**: +1. ✅ Vérifie `users_email_key` ou `idx_users_email` → `ErrUserAlreadyExists` +2. ✅ Vérifie `users_username_key` ou `idx_users_username` → `errors.New("username already exists")` +3. ✅ Vérifie `users_slug_key` ou `idx_users_slug` → `errors.New("username unavailable (slug collision)")` +4. ✅ Vérifie "unique constraint" ou "duplicate key" → `ErrUserAlreadyExists` +5. ❌ **SINON** : `fmt.Errorf("database error: %w", err)` → **ERREUR GÉNÉRIQUE** + +**PROBLÈME CRITIQUE**: +- Si l'erreur PostgreSQL n'est **PAS** une violation de contrainte unique, elle est retournée comme "database error" +- Cette erreur est ensuite encapsulée dans le handler comme code 9000 +- **L'erreur réelle est perdue dans les logs** + +--- + +### 3. Modèle User (`internal/models/user.go`) + +**Structure**: +```go +type User struct { + ID uuid.UUID + Username string `gorm:"not null;size:30"` + Slug string `gorm:"size:255"` // ⚠️ NULLABLE + Email string `gorm:"not null;size:255"` + PasswordHash string `gorm:"size:255"` + Role string `gorm:"not null;default:'user'"` // ⚠️ ENUM + // ... +} +``` + +**Problèmes potentiels**: +1. **Slug NULLABLE** : L'index unique `idx_users_slug` s'applique même si `slug IS NULL` + - PostgreSQL permet plusieurs NULL dans un index unique + - Mais si un utilisateur existe avec `slug = NULL`, l'insertion peut échouer selon la version PostgreSQL +2. **Role ENUM** : Le champ `role` est de type `public.user_role` (ENUM) + - Valeur "user" doit exister dans l'ENUM ✅ + - Mais si l'ENUM n'existe pas en base, l'insertion échouera + +--- + +### 4. Schéma Base de Données (`migrations/010_auth_and_users.sql`) + +**Table `users`**: +```sql +CREATE TABLE public.users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL, + username VARCHAR(30) NOT NULL, + slug VARCHAR(255), -- ⚠️ NULLABLE + role public.user_role NOT NULL DEFAULT 'user', -- ⚠️ ENUM + -- ... +); +``` + +**Index uniques**: +```sql +CREATE UNIQUE INDEX idx_users_email ON public.users(email) WHERE deleted_at IS NULL; +CREATE UNIQUE INDEX idx_users_username ON public.users(username) WHERE deleted_at IS NULL; +CREATE UNIQUE INDEX idx_users_slug ON public.users(slug) WHERE deleted_at IS NULL; -- ⚠️ NULLABLE +``` + +**Contraintes CHECK**: +```sql +CONSTRAINT chk_users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') +CONSTRAINT chk_users_username_format CHECK (username ~* '^[a-zA-Z0-9_]{3,30}$') +``` + +**Problèmes potentiels**: +1. **Contrainte username** : Le regex exige 3-30 caractères + - Si `username` a moins de 3 caractères, l'insertion échouera + - **MAIS** : La validation Go devrait capturer cela avant +2. **Contrainte email** : Le regex exige un format email valide + - Si `email` ne correspond pas, l'insertion échouera + - **MAIS** : La validation Go devrait capturer cela avant + +--- + +## 🔴 CAUSES PROBABLES DE L'ÉCHEC + +### Cause #1 : Erreur PostgreSQL non capturée ❓ + +**Hypothèse** : L'erreur PostgreSQL n'est **PAS** une violation de contrainte unique, mais une autre erreur (ex: contrainte CHECK, type ENUM, etc.) + +**Preuve**: +- Le code vérifie uniquement les violations de contrainte unique +- Toutes les autres erreurs sont retournées comme "database error" +- Cette erreur est ensuite encapsulée dans le handler comme code 9000 + +**Solution**: +1. Ajouter des logs détaillés de l'erreur PostgreSQL +2. Vérifier les logs du backend (`/tmp/backend.log` ou stdout) +3. Capturer l'erreur PostgreSQL complète avant de l'encapsuler + +### Cause #2 : Contrainte CHECK échoue ❓ + +**Hypothèse** : La contrainte `chk_users_username_format` ou `chk_users_email_format` échoue + +**Preuve**: +- Les contraintes CHECK ne sont pas vérifiées dans le code Go +- Si le username/email ne correspond pas au regex, PostgreSQL rejette l'insertion +- L'erreur PostgreSQL serait quelque chose comme: `ERROR: new row for relation "users" violates check constraint "chk_users_username_format"` + +**Solution**: +1. Vérifier que la validation Go correspond aux contraintes PostgreSQL +2. Ajouter des logs pour capturer les erreurs de contrainte CHECK + +### Cause #3 : ENUM `user_role` n'existe pas ❓ + +**Hypothèse** : L'ENUM `public.user_role` n'existe pas dans la base de données + +**Preuve**: +- Le code définit `Role: "user"` mais si l'ENUM n'existe pas, PostgreSQL rejette l'insertion +- L'erreur PostgreSQL serait: `ERROR: type "public.user_role" does not exist` + +**Solution**: +1. Vérifier que la migration `001_extensions_and_types.sql` a été exécutée +2. Vérifier que l'ENUM existe: `SELECT * FROM pg_type WHERE typname = 'user_role';` + +### Cause #4 : Slug NULL ou collision ❓ + +**Hypothèse** : Le slug généré est NULL ou entre en collision avec un slug existant + +**Preuve**: +- L'index unique `idx_users_slug` s'applique même si `slug IS NULL` +- Si un utilisateur existe avec `slug = NULL`, l'insertion peut échouer +- **MAIS** : Le code génère toujours un slug, donc peu probable + +**Solution**: +1. Vérifier que le slug n'est jamais vide +2. Ajouter un fallback si le slug est vide + +### Cause #5 : Timeout du contexte ❓ + +**Hypothèse** : Le contexte expire avant la fin de l'insertion + +**Preuve**: +- Le handler crée un contexte avec timeout de 5 secondes +- Si l'insertion prend plus de 5 secondes, le contexte expire +- L'erreur serait: `context deadline exceeded` + +**Solution**: +1. Augmenter le timeout +2. Vérifier les performances de la base de données + +--- + +## 🛠️ ACTIONS CORRECTIVES RECOMMANDÉES + +### Action 1 : Améliorer les logs d'erreur ⚡ **PRIORITÉ HAUTE** + +**Fichier**: `internal/core/auth/service.go` (ligne 229-279) + +**Changement**: +```go +result := s.db.WithContext(ctx).Omit("Roles", "TrackLikes").Create(user) +if result.Error != nil { + err := result.Error + + // 🔴 AJOUTER : Log l'erreur PostgreSQL complète + s.logger.Error("Failed to create user in database - FULL ERROR", + zap.Error(err), + zap.String("error_type", fmt.Sprintf("%T", err)), + zap.String("error_string", err.Error()), + zap.String("email", email), + zap.String("username", username), + zap.String("slug", slug), + zap.String("role", user.Role), + ) + + // Vérifier les erreurs PostgreSQL spécifiques + errStr := err.Error() + + // Vérifier contrainte CHECK + if strings.Contains(errStr, "violates check constraint") { + if strings.Contains(errStr, "chk_users_username_format") { + return nil, nil, errors.New("username format invalid: must be 3-30 alphanumeric characters") + } + if strings.Contains(errStr, "chk_users_email_format") { + return nil, nil, errors.New("email format invalid") + } + } + + // Vérifier type ENUM + if strings.Contains(errStr, "does not exist") && strings.Contains(errStr, "user_role") { + return nil, nil, fmt.Errorf("database schema error: user_role enum missing") + } + + // Vérifier timeout + if strings.Contains(errStr, "context deadline exceeded") || strings.Contains(errStr, "timeout") { + return nil, nil, fmt.Errorf("database operation timed out") + } + + // ... reste du code existant +} +``` + +### Action 2 : Vérifier l'ENUM en base ⚡ **PRIORITÉ HAUTE** + +**Commande**: +```bash +docker-compose exec db psql -U veza_user -d veza_db -c "SELECT * FROM pg_type WHERE typname = 'user_role';" +``` + +**Si l'ENUM n'existe pas**: +```bash +docker-compose exec db psql -U veza_user -d veza_db -f migrations/001_extensions_and_types.sql +``` + +### Action 3 : Vérifier les contraintes CHECK ⚡ **PRIORITÉ MOYENNE** + +**Commande**: +```bash +docker-compose exec db psql -U veza_user -d veza_db -c "SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'c';" +``` + +**Vérifier que les regex correspondent à la validation Go** + +### Action 4 : Capturer l'erreur PostgreSQL complète ⚡ **PRIORITÉ HAUTE** + +**Fichier**: `internal/handlers/auth.go` (ligne 178-179) + +**Changement**: +```go +default: + commonHandler.logger.Error("Registration failed - FULL ERROR", + zap.Error(err), + zap.String("error_type", fmt.Sprintf("%T", err)), + zap.String("error_string", err.Error()), + ) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create user", err)) +``` + +--- + +## 📊 DIAGNOSTIC IMMÉDIAT + +### Étape 1 : Vérifier les logs du backend + +```bash +tail -100 /tmp/backend.log | grep -A 10 "REGISTER\|DB INSERT ERROR" +``` + +### Étape 2 : Tester l'insertion directe en base + +```bash +docker-compose exec db psql -U veza_user -d veza_db <