veza/REGISTER_ENDPOINT_AUDIT.md
senke 08596eda71 [AUDIT] Analyse complète de l'endpoint d'enregistrement
- 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
2026-01-04 01:44:15 +01:00

13 KiB

🔍 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 ⚠️

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

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

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_emailErrUserAlreadyExists
  2. Vérifie users_username_key ou idx_users_usernameerrors.New("username already exists")
  3. Vérifie users_slug_key ou idx_users_slugerrors.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:

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:

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:

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:

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:

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:

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:

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:

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:

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

tail -100 /tmp/backend.log | grep -A 10 "REGISTER\|DB INSERT ERROR"

Étape 2 : Tester l'insertion directe en base

docker-compose exec db psql -U veza_user -d veza_db <<EOF
INSERT INTO users (id, email, username, slug, password_hash, role, is_active, is_verified, is_banned, token_version, login_count, created_at, updated_at)
VALUES (
    gen_random_uuid(),
    'test-direct@example.com',
    'testdirect',
    'testdirect',
    'hashed_password_here',
    'user'::user_role,
    true,
    true,
    false,
    0,
    0,
    NOW(),
    NOW()
);
EOF

Étape 3 : Vérifier l'ENUM

docker-compose exec db psql -U veza_user -d veza_db -c "SELECT * FROM pg_type WHERE typname = 'user_role';"

Étape 4 : Vérifier les contraintes

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;"

CONCLUSION

Problème principal : L'erreur PostgreSQL réelle est perdue dans les logs et encapsulée dans une erreur générique code 9000.

Solution immédiate : Ajouter des logs détaillés pour capturer l'erreur PostgreSQL complète avant de l'encapsuler.

Actions prioritaires:

  1. Améliorer les logs d'erreur dans service.go
  2. Vérifier l'ENUM user_role en base
  3. Capturer l'erreur PostgreSQL complète dans le handler
  4. Vérifier les contraintes CHECK

Prochaines étapes:

  1. Exécuter les commandes de diagnostic ci-dessus
  2. Analyser les logs du backend
  3. Appliquer les corrections recommandées
  4. Re-tester l'endpoint d'enregistrement