13 KiB
🔧 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_nameetlast_namedepuis la tableorganization_members - Cette table ne contient PAS ces colonnes - elles n'existent que dans
user_metadatade la tableauth.users
Solution appliquée :
Fichier modifié : /app/(app)/staff/tickets/[id]/page.tsx
// ❌ 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 :
user_metadata.display_name⭐ PRIORITAIRE (c'est ici que Supabase Auth stocke le prénom)user_metadata.first_name+user_metadata.last_name- Email de l'utilisateur
- "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.tsutilisaitcreateRouteHandlerClientqui est deprecated - Import manquant :
cookiesfrom "next/headers"
Cause 2 : Permissions insuffisantes (PROBLÈME PRINCIPAL)
- Erreur :
AuthApiError: User not allowedavec codenot_admin sb.auth.admin.getUserById()nécessite des permissions administrateurcreateSbServer()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
// ❌ 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 normalescreateSbServiceRole(): Utilise la clé service_role (admin) pour les opérations privilégiéesauth.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 :
// ❌ 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 :
-
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
-
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
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_KEYau 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 :
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_namelast_nameemaildisplay_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_nameen 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
-
Ouvrez un ticket en mode staff
-
Regardez les logs dans le terminal du serveur Next.js
-
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 -
Si vous voyez
user_metadata: {}(vide), cela signifie que le champ "Display name" n'est pas rempli dans Supabase Auth -
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)
- ✅ Compilation TypeScript sans erreur
- ✅ Build Next.js réussi
- ✅ Route API
/api/tickets/[id]/recipient-infofonctionnelle - ✅ Récupération correcte du nom depuis
user_metadata - ✅ Modal de confirmation s'affiche correctement
- ✅ Affichage du nom du créateur sur la page staff
📚 Leçons apprises
⚠️ Important à retenir
- Ne jamais supposer la structure d'une table sans vérifier le schéma réel
- Les informations utilisateur (nom, prénom) sont dans
user_metadata, pas dansorganization_members - Utiliser
createSbServerau lieu decreateRouteHandlerClient(deprecated) - Toujours tester les routes API avant d'implémenter le frontend
- 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_metadatapour les informations personnelles de l'utilisateur
🔗 Fichiers concernés par les corrections
/app/(app)/staff/tickets/[id]/page.tsx- Affichage du nom du créateur/app/api/tickets/[id]/recipient-info/route.ts- Route API de récupération des infos/lib/supabaseServer.ts- FonctioncreateSbServerutilisée
Documentation mise à jour le 14 octobre 2025