- 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
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:
- ✅ Bind et validation JSON (
BindAndValidateJSON) - ✅ Création d'un contexte avec timeout (5 secondes)
- ✅ Appel
authService.Register(ctx, email, username, password) - ❌ ÉCHEC ICI : Retourne erreur générique code 9000
- ⏭️ Création de session (non-bloquant si échoue)
- ⏭️ 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
ErrUserAlreadyExistssi 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:
- ✅ Vérifie
users_email_keyouidx_users_email→ErrUserAlreadyExists - ✅ Vérifie
users_username_keyouidx_users_username→errors.New("username already exists") - ✅ Vérifie
users_slug_keyouidx_users_slug→errors.New("username unavailable (slug collision)") - ✅ Vérifie "unique constraint" ou "duplicate key" →
ErrUserAlreadyExists - ❌ 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:
- Slug NULLABLE : L'index unique
idx_users_slugs'applique même sislug 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
- Role ENUM : Le champ
roleest de typepublic.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:
- Contrainte username : Le regex exige 3-30 caractères
- Si
usernamea moins de 3 caractères, l'insertion échouera - MAIS : La validation Go devrait capturer cela avant
- Si
- Contrainte email : Le regex exige un format email valide
- Si
emailne correspond pas, l'insertion échouera - MAIS : La validation Go devrait capturer cela avant
- Si
🔴 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:
- Ajouter des logs détaillés de l'erreur PostgreSQL
- Vérifier les logs du backend (
/tmp/backend.logou stdout) - 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:
- Vérifier que la validation Go correspond aux contraintes PostgreSQL
- 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:
- Vérifier que la migration
001_extensions_and_types.sqla été exécutée - 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_slugs'applique même sislug 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:
- Vérifier que le slug n'est jamais vide
- 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:
- Augmenter le timeout
- 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:
- ⚡ Améliorer les logs d'erreur dans
service.go - ⚡ Vérifier l'ENUM
user_roleen base - ⚡ Capturer l'erreur PostgreSQL complète dans le handler
- ⚡ Vérifier les contraintes CHECK
Prochaines étapes:
- Exécuter les commandes de diagnostic ci-dessus
- Analyser les logs du backend
- Appliquer les corrections recommandées
- Re-tester l'endpoint d'enregistrement