[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
This commit is contained in:
parent
13cb5f4302
commit
08596eda71
1 changed files with 395 additions and 0 deletions
395
REGISTER_ENDPOINT_AUDIT.md
Normal file
395
REGISTER_ENDPOINT_AUDIT.md
Normal file
|
|
@ -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 <<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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```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;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 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
|
||||
|
||||
Loading…
Reference in a new issue