Cette implémentation garantit que les profils clients sont isolés par organisation, permettant à différentes organisations d'avoir leurs propres clients même avec les mêmes informations (email identique).
- Agence A (Org X) enregistre Client Z → Le profil est lié à Org X
- Agence B (Org X) recherche Client Z → ✅ Accès autorisé (même organisation)
- Agence C (Org Y) recherche Client Z → ❌ Client non trouvé → Création d'un nouveau profil pour Org Y
1. Schéma de Base de Données (drizzle/schema.ts)
- ✅ Ajout du champ
organizationId(clé étrangère versorganizations) - ✅ Suppression de la contrainte
uniquesuremailseul - ✅ Ajout d'une contrainte composite
unique(email, organizationId) - ✅ Ajout de la relation vers
organization
export const customers = pgTable('customers', {
id: uuid('id').primaryKey().defaultRandom(),
organizationId: uuid('organization_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
email: text('email').notNull(),
// ... autres champs
}, (table) => ({
// Contrainte d'unicité : un email unique par organisation
emailOrgUnique: unique().on(table.email, table.organizationId),
}));2. Migration SQL (drizzle/migrations/0002_add_organization_to_customers.sql)
La migration a été créée avec les étapes suivantes :
- Ajout de la colonne
organization_id(nullable au départ) - Suppression de la contrainte
uniquesuremail - Ajout de la clé étrangère vers
organizations - Ajout de la contrainte composite
unique(email, organization_id)
ALTER TABLE customers ALTER COLUMN organization_id SET NOT NULL, vous devez assigner tous les clients existants à une organisation.
3. Nouvelle Fonction Helper (lib/session.ts:60-63)
Ajout de getCurrentOrganizationId() :
export const getCurrentOrganizationId = cache(async () => {
const team = await getCurrentTeam();
return team?.organizationId || null;
});Recherche de Clients (app/(dashboard)/reservations/actions.ts:330-370)
// Recherche limitée à l'organisation actuelle
const customersList = await db
.select()
.from(customers)
.where(
and(
eq(customers.organizationId, organizationId),
or(/* critères de recherche */)
)
);Création de Clients (app/(dashboard)/reservations/actions.ts:372-414)
// Vérification de l'unicité au sein de l'organisation
const existingCustomer = await db.query.customers.findFirst({
where: and(
eq(customers.email, data.email.toLowerCase()),
eq(customers.organizationId, organizationId)
),
});
// Création avec organizationId
const [customer] = await db.insert(customers).values({
organizationId,
email: data.email.toLowerCase(),
// ...
});Route Publique (app/api/reservations/public/[token]/customer/route.ts)
- Récupération de l'
organizationIdvia la réservation - Vérification de l'existence du client au sein de l'organisation
- Création avec
organizationIdsi nécessaire
Liste des Clients (app/(dashboard)/customers/page.tsx:10-33)
// Filtrage par organisation
.where(eq(customers.organizationId, organizationId))Détails d'un Client (app/(dashboard)/customers/[id]/page.tsx:36-52)
// Vérification que le client appartient à l'organisation
const customer = await db.query.customers.findFirst({
where: and(
eq(customers.id, params.id),
eq(customers.organizationId, organizationId)
),
});Communications (app/api/communications/route.ts:61-77)
// Vérification que le client appartient à l'organisation avant envoi
const customer = await db.query.customers.findFirst({
where: and(
eq(customers.id, customerId),
eq(customers.organizationId, organizationId)
),
});La migration a été appliquée avec succès le 29 novembre 2025. Voici les étapes qui ont été suivies :
# Créer un backup avant la migration (RECOMMANDÉ en production)
pg_dump your_database > backup_before_customer_isolation.sqlpsql "$DATABASE_URL" -c "SELECT id, name FROM organizations;"Résultat :
- Agence Paris :
c25a6784-783a-49bd-9f72-fbe5e1225782 - Angence paris :
66602f8b-2433-4c49-b92d-61d7454ac49c
# Appliquer la migration manuellement (car drizzle-kit migrate échouait)
psql "$DATABASE_URL" < drizzle/migrations/0002_add_organization_to_customers.sql# Tous les clients existants ont été assignés à Agence Paris
psql "$DATABASE_URL" -c "UPDATE customers SET organization_id = 'c25a6784-783a-49bd-9f72-fbe5e1225782' WHERE organization_id IS NULL;"Résultat : 4 clients mis à jour
psql "$DATABASE_URL" -c "ALTER TABLE customers ALTER COLUMN organization_id SET NOT NULL;"# Vérifier qu'il n'y a plus de clients sans organisation
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM customers WHERE organization_id IS NULL;"
# Résultat : 0
# Vérifier les contraintes
psql "$DATABASE_URL" -c "SELECT constraint_name, constraint_type FROM information_schema.table_constraints WHERE table_name = 'customers';"Contraintes vérifiées :
- ✅
customers_organization_id_organizations_id_fk(FOREIGN KEY) - ✅
customers_email_organization_id_unique(UNIQUE) - ✅
customers_pkey(PRIMARY KEY)
- ❌ Un email = un seul client globalement
- ❌ Toutes les organisations voient le même client
- ❌ Risque de fuite de données entre organisations
- ✅ Un email peut exister dans plusieurs organisations
- ✅ Chaque organisation a ses propres clients
- ✅ Isolation complète des données sensibles
- ✅ Une agence ne peut pas voir les clients d'une autre organisation
- drizzle/schema.ts - Schéma de base de données
- drizzle/migrations/0002_add_organization_to_customers.sql - Migration SQL
- lib/session.ts - Helper pour obtenir l'organizationId
- app/(dashboard)/reservations/actions.ts - Recherche et création de clients
- app/api/reservations/public/[token]/customer/route.ts - Création publique de clients
- app/api/customers/route.ts - Liste des clients
- app/(dashboard)/customers/page.tsx - Page liste des clients
- app/(dashboard)/customers/[id]/page.tsx - Page détail d'un client
- app/api/communications/route.ts - Communications avec vérification d'appartenance
-
Test d'Isolation :
- Créer un client avec email
test@example.comdans Org A - Créer un client avec le même email dans Org B
- Vérifier que les deux clients sont distincts
- Vérifier que Org A ne voit que son client
- Créer un client avec email
-
Test de Recherche :
- Se connecter en tant qu'utilisateur de Org A
- Rechercher un client qui existe uniquement dans Org B
- Vérifier qu'aucun résultat n'est retourné
-
Test de Communication :
- Tenter d'envoyer un message à un client d'une autre organisation
- Vérifier que l'erreur "Customer not found or does not belong to your organization" est retournée
-
Test de Réservation Publique :
- Créer une réservation via le lien magique
- Vérifier que le client est créé avec le bon
organizationId - Vérifier que si le client existe déjà dans l'organisation, il est réutilisé