espace-paie-odentas/SUPPORT_TICKET_NOTIFICATIONS_FIXES.md

371 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔧 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*