espace-paie-odentas/SUPPORT_TICKET_NOTIFICATIONS_FIXES.md

13 KiB
Raw Permalink Blame History

🔧 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

// ❌ 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

// ❌ 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 :

// ❌ 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

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 :

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