371 lines
13 KiB
Markdown
371 lines
13 KiB
Markdown
# 🔧 Corrections Support Ticket Notifications
|
||
|
||
## Problèmes identifiés et résolus
|
||
|
||
### ❌ Problème 1 : "Utilisateur inconnu" sur la page staff
|
||
|
||
**Symptôme :**
|
||
- Sur la page `/staff/tickets/[id]`, le nom du créateur s'affichait comme "Utilisateur inconnu"
|
||
- L'organisation s'affichait correctement
|
||
|
||
**Cause :**
|
||
- Le code essayait de récupérer `first_name` et `last_name` depuis la table `organization_members`
|
||
- **Cette table ne contient PAS ces colonnes** - elles n'existent que dans `user_metadata` de la table `auth.users`
|
||
|
||
**Solution appliquée :**
|
||
|
||
**Fichier modifié :** `/app/(app)/staff/tickets/[id]/page.tsx`
|
||
|
||
```typescript
|
||
// ❌ AVANT (incorrect)
|
||
const { data: orgMember } = await sb
|
||
.from("organization_members")
|
||
.select("first_name, last_name") // Ces colonnes n'existent pas !
|
||
.eq("user_id", ticket.created_by)
|
||
.eq("org_id", ticket.org_id)
|
||
.maybeSingle();
|
||
|
||
// ✅ APRÈS (correct)
|
||
const { data: creatorUser } = await sb.auth.admin.getUserById(ticket.created_by);
|
||
if (creatorUser?.user) {
|
||
const metadata = creatorUser.user.user_metadata;
|
||
|
||
// ⭐ Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
|
||
if (metadata?.display_name) {
|
||
creatorName = metadata.display_name;
|
||
}
|
||
// Priorité 2: first_name + last_name
|
||
else if (metadata?.first_name) {
|
||
creatorName = metadata.last_name
|
||
? `${metadata.first_name} ${metadata.last_name}`
|
||
: metadata.first_name;
|
||
}
|
||
// Priorité 3: email
|
||
else {
|
||
creatorName = creatorUser.user.email || "Utilisateur inconnu";
|
||
}
|
||
}
|
||
```
|
||
|
||
**Hiérarchie de fallback :**
|
||
1. `user_metadata.display_name` ⭐ **PRIORITAIRE** (c'est ici que Supabase Auth stocke le prénom)
|
||
2. `user_metadata.first_name` + `user_metadata.last_name`
|
||
3. Email de l'utilisateur
|
||
4. "Utilisateur inconnu"
|
||
|
||
**⚠️ Important :** Dans Supabase Auth, le champ "Display name" dans l'interface admin correspond à `user_metadata.display_name` dans le code.
|
||
|
||
---
|
||
|
||
### ❌ Problème 2 : Erreur 404 lors de la validation du message
|
||
|
||
**Symptôme :**
|
||
- Lors du clic sur "Valider" pour envoyer une réponse en mode staff
|
||
- Console : `Failed to load resource: the server responded with a status of 404 (Not Found)`
|
||
- Erreur : `{"error":"Email introuvable"}`
|
||
- Aucun modal ne s'affichait
|
||
|
||
**Cause 1 : Import incorrect de Supabase**
|
||
- Le fichier `/app/api/tickets/[id]/recipient-info/route.ts` utilisait `createRouteHandlerClient` qui est **deprecated**
|
||
- Import manquant : `cookies` from "next/headers"
|
||
|
||
**Cause 2 : Permissions insuffisantes (PROBLÈME PRINCIPAL)**
|
||
- Erreur : `AuthApiError: User not allowed` avec code `not_admin`
|
||
- `sb.auth.admin.getUserById()` nécessite des **permissions administrateur**
|
||
- `createSbServer()` utilise la clé **anon** (publique), pas la clé **service_role** (admin)
|
||
- L'appel à `getUserById()` était bloqué par Supabase pour des raisons de sécurité
|
||
|
||
**Solution 1 :**
|
||
|
||
**Fichier modifié :** `/app/api/tickets/[id]/recipient-info/route.ts`
|
||
|
||
```typescript
|
||
// ❌ AVANT (deprecated + permissions insuffisantes)
|
||
import { cookies } from "next/headers";
|
||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||
|
||
const sb = createRouteHandlerClient({ cookies });
|
||
const { data: creatorUser } = await sb.auth.admin.getUserById(ticket.created_by);
|
||
// ❌ Erreur: AuthApiError: User not allowed (code: not_admin)
|
||
|
||
// ✅ APRÈS (correct + permissions admin)
|
||
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
|
||
|
||
const sb = createSbServer(); // Pour l'authentification et les queries normales
|
||
const sbAdmin = createSbServiceRole(); // Pour les opérations admin
|
||
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
|
||
// ✅ Fonctionne avec la clé service_role
|
||
```
|
||
|
||
**Explication :**
|
||
- `createSbServer()` : Utilise la clé **anon** (publique) pour les opérations utilisateur normales
|
||
- `createSbServiceRole()` : Utilise la clé **service_role** (admin) pour les opérations privilégiées
|
||
- `auth.admin.getUserById()` nécessite **obligatoirement** la clé service_role
|
||
|
||
**Cause 2 : Structure de données incorrecte**
|
||
- Même erreur que le problème 1 : tentative de récupération depuis `organization_members`
|
||
|
||
**Solution 2 :**
|
||
|
||
```typescript
|
||
// ❌ AVANT (incorrect)
|
||
const { data: orgMember } = await sb
|
||
.from("organization_members")
|
||
.select("first_name, last_name") // Ces colonnes n'existent pas !
|
||
.eq("user_id", ticket.created_by)
|
||
.eq("org_id", ticket.org_id)
|
||
.maybeSingle();
|
||
|
||
// ✅ APRÈS (correct + utilisation du service role)
|
||
const sbAdmin = createSbServiceRole();
|
||
const { data: creatorUser } = await sbAdmin.auth.admin.getUserById(ticket.created_by);
|
||
const metadata = creatorUser.user.user_metadata;
|
||
|
||
// ⭐ Priorité 1: display_name (c'est là que Supabase Auth stocke le prénom)
|
||
if (metadata?.display_name) {
|
||
name = metadata.display_name;
|
||
}
|
||
// Priorité 2: first_name + last_name
|
||
else if (metadata?.first_name) {
|
||
name = metadata.last_name
|
||
? `${metadata.first_name} ${metadata.last_name}`
|
||
: metadata.first_name;
|
||
}
|
||
// Sinon: utiliser l'email par défaut
|
||
```
|
||
|
||
---
|
||
|
||
## 🔐 Permissions Supabase et Service Role
|
||
|
||
### Pourquoi deux clients Supabase ?
|
||
|
||
Dans une application Supabase, il existe **deux types de clés API** :
|
||
|
||
1. **Clé `anon` (publique)** :
|
||
- Utilisée côté client et pour les opérations utilisateur normales
|
||
- Respecte les Row Level Security (RLS) policies
|
||
- **Ne peut PAS** accéder aux fonctions admin comme `getUserById()`
|
||
- Sécurisée pour être exposée au navigateur
|
||
|
||
2. **Clé `service_role` (admin)** :
|
||
- Utilisée uniquement côté serveur
|
||
- **Bypass les RLS policies**
|
||
- **Peut** accéder aux fonctions admin
|
||
- **NE DOIT JAMAIS** être exposée au client
|
||
|
||
### Quand utiliser quelle clé ?
|
||
|
||
| Opération | Client à utiliser | Pourquoi |
|
||
|-----------|-------------------|----------|
|
||
| Authentifier l'utilisateur | `createSbServer()` | Clé anon suffit |
|
||
| Vérifier si l'utilisateur est staff | `createSbServer()` | RLS policy autorise |
|
||
| Récupérer des tickets | `createSbServer()` | RLS policy autorise |
|
||
| **Récupérer un utilisateur par ID** | `createSbServiceRole()` | **Nécessite admin** |
|
||
| **Accéder aux métadonnées utilisateur** | `createSbServiceRole()` | **Nécessite admin** |
|
||
| Envoyer un email | `createSbServiceRole()` | Généralement utilisé |
|
||
|
||
### Exemple d'utilisation correcte
|
||
|
||
```typescript
|
||
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
||
// 1. Authentification et vérification des permissions avec la clé anon
|
||
const sb = createSbServer();
|
||
const { data: { user } } = await sb.auth.getUser();
|
||
if (!user) return new NextResponse("Unauthorized", { status: 401 });
|
||
|
||
const { data: staffUser } = await sb
|
||
.from("staff_users")
|
||
.select("is_staff")
|
||
.eq("user_id", user.id)
|
||
.maybeSingle();
|
||
|
||
if (!staffUser?.is_staff) {
|
||
return new NextResponse("Forbidden", { status: 403 });
|
||
}
|
||
|
||
// 2. Opérations privilégiées avec la clé service_role
|
||
const sbAdmin = createSbServiceRole();
|
||
const { data: userData } = await sbAdmin.auth.admin.getUserById(someUserId);
|
||
|
||
return NextResponse.json({ data: userData });
|
||
}
|
||
```
|
||
|
||
### ⚠️ Sécurité importante
|
||
|
||
- **JAMAIS** exposer `SUPABASE_SERVICE_ROLE_KEY` au client
|
||
- Toujours vérifier les permissions **AVANT** d'utiliser `createSbServiceRole()`
|
||
- Utiliser `createSbServiceRole()` uniquement dans les API routes (côté serveur)
|
||
|
||
---
|
||
|
||
## 📊 Structure réelle de la base de données
|
||
|
||
### Mapping Supabase Auth UI → Code
|
||
|
||
**Dans l'interface Supabase Auth (Admin Panel) :**
|
||
- **"Display name"** → `user_metadata.display_name` ⭐
|
||
- **"Email"** → `email`
|
||
|
||
**Dans le code :**
|
||
```typescript
|
||
const { data: userData } = await sb.auth.admin.getUserById(userId);
|
||
|
||
// Accès aux données :
|
||
userData.user.email // Email de l'utilisateur
|
||
userData.user.user_metadata // Objet contenant les métadonnées
|
||
userData.user.user_metadata.display_name // Le prénom (champ "Display name" dans l'UI)
|
||
userData.user.user_metadata.first_name // Optionnel, si configuré
|
||
userData.user.user_metadata.last_name // Optionnel, si configuré
|
||
```
|
||
|
||
### Table `organization_members`
|
||
**Colonnes disponibles :**
|
||
- `id` (UUID)
|
||
- `org_id` (UUID)
|
||
- `user_id` (UUID)
|
||
- `role` (TEXT)
|
||
- `revoked` (BOOLEAN)
|
||
- `revoked_at` (TIMESTAMP)
|
||
- `created_at` (TIMESTAMP)
|
||
|
||
**❌ PAS de colonnes :**
|
||
- `first_name`
|
||
- `last_name`
|
||
- `email`
|
||
- `display_name`
|
||
|
||
### Table `auth.users` (via `auth.admin.getUserById`)
|
||
**Données disponibles :**
|
||
- `id` (UUID)
|
||
- `email` (TEXT)
|
||
- `email_confirmed_at` (TIMESTAMP)
|
||
- `user_metadata` (JSONB) :
|
||
- `first_name` ✅
|
||
- `last_name` ✅
|
||
- `display_name` ✅
|
||
- autres champs personnalisés
|
||
|
||
---
|
||
|
||
## ✅ Résultat après corrections
|
||
|
||
### Sur la page staff `/staff/tickets/[id]`
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ Organisation: Nom de l'entreprise │
|
||
│ Ouvert par: Jean Dupont │ ← Maintenant correct !
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### Dans le modal de confirmation
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ Confirmer l'envoi de votre réponse │
|
||
│ │
|
||
│ Destinataire │
|
||
│ 📧 jean.dupont@example.com │
|
||
│ Jean Dupont │ ← Maintenant correct !
|
||
│ │
|
||
│ Message │
|
||
│ [Aperçu du message...] │
|
||
│ │
|
||
│ ℹ️ La notification sera envoyée à │
|
||
│ l'adresse personnelle de │
|
||
│ l'utilisateur │
|
||
│ │
|
||
│ [Annuler] [Envoyer ✉️] │ ← Fonctionne maintenant !
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 Vérifications effectuées
|
||
|
||
✅ **Correction appliquée :**
|
||
- Ordre de priorité modifié : `display_name` en premier
|
||
- Logging ajouté pour déboguer les métadonnées utilisateur
|
||
- Les deux fichiers mis à jour :
|
||
- `/app/(app)/staff/tickets/[id]/page.tsx`
|
||
- `/app/api/tickets/[id]/recipient-info/route.ts`
|
||
|
||
### 🐛 Comment déboguer si le problème persiste
|
||
|
||
1. **Ouvrez un ticket en mode staff**
|
||
2. **Regardez les logs dans le terminal du serveur Next.js**
|
||
3. **Cherchez ces lignes :**
|
||
```
|
||
📋 [ticket page] User ID: xxx-xxx-xxx
|
||
📋 [ticket page] Email: user@example.com
|
||
📋 [ticket page] User metadata: { ... }
|
||
✅ [ticket page] Nom trouvé dans display_name: Jean
|
||
```
|
||
|
||
4. **Si vous voyez `user_metadata: {}`** (vide), cela signifie que le champ "Display name" n'est pas rempli dans Supabase Auth
|
||
|
||
5. **Pour vérifier dans Supabase directement :**
|
||
- Allez dans Authentication → Users
|
||
- Cliquez sur l'utilisateur concerné
|
||
- Vérifiez que le champ **"Display name"** est bien rempli
|
||
- Si vide, modifiez-le et réessayez
|
||
|
||
### 🔧 Logs disponibles
|
||
|
||
Les logs suivants s'affichent maintenant dans le terminal :
|
||
|
||
**Sur la page staff `/staff/tickets/[id]` :**
|
||
```
|
||
📋 [ticket page] User ID: <uuid>
|
||
📋 [ticket page] Email: <email>
|
||
📋 [ticket page] User metadata: <json>
|
||
✅ [ticket page] Nom trouvé dans display_name: <nom>
|
||
```
|
||
|
||
**Dans l'API recipient-info :**
|
||
```
|
||
📋 [recipient-info] User metadata: <json>
|
||
✅ [recipient-info] Nom trouvé dans display_name: <nom>
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 Vérifications effectuées (Build)
|
||
|
||
1. ✅ Compilation TypeScript sans erreur
|
||
2. ✅ Build Next.js réussi
|
||
3. ✅ Route API `/api/tickets/[id]/recipient-info` fonctionnelle
|
||
4. ✅ Récupération correcte du nom depuis `user_metadata`
|
||
5. ✅ Modal de confirmation s'affiche correctement
|
||
6. ✅ Affichage du nom du créateur sur la page staff
|
||
|
||
---
|
||
|
||
## 📚 Leçons apprises
|
||
|
||
### ⚠️ Important à retenir
|
||
|
||
1. **Ne jamais supposer la structure d'une table** sans vérifier le schéma réel
|
||
2. **Les informations utilisateur (nom, prénom)** sont dans `user_metadata`, pas dans `organization_members`
|
||
3. **Utiliser `createSbServer`** au lieu de `createRouteHandlerClient` (deprecated)
|
||
4. **Toujours tester les routes API** avant d'implémenter le frontend
|
||
5. **Vérifier les logs de la console** pour identifier les 404 et autres erreurs réseau
|
||
|
||
### 🎯 Bonnes pratiques
|
||
|
||
- Utiliser `auth.admin.getUserById()` pour accéder aux données complètes de l'utilisateur
|
||
- Implémenter une hiérarchie de fallback pour l'affichage des noms
|
||
- Gérer les cas où les métadonnées sont absentes
|
||
- Préférer `user_metadata` pour les informations personnelles de l'utilisateur
|
||
|
||
---
|
||
|
||
## 🔗 Fichiers concernés par les corrections
|
||
|
||
1. `/app/(app)/staff/tickets/[id]/page.tsx` - Affichage du nom du créateur
|
||
2. `/app/api/tickets/[id]/recipient-info/route.ts` - Route API de récupération des infos
|
||
3. `/lib/supabaseServer.ts` - Fonction `createSbServer` utilisée
|
||
|
||
---
|
||
|
||
*Documentation mise à jour le 14 octobre 2025*
|