Rôles, Entités & Contrôle d'Accès — Automobile Business Center
Document généré par introspection de la base de données et du code source. Date : 10 mars 2026
Table des matières
- Vue d'ensemble des entités
- Rôles utilisateurs (UserType)
- Rôles administrateurs (AdminRole)
- Entités système non-utilisateurs
- Flux d'authentification & vérification
- Matrice d'accès par rôle (RLS)
- Changements de rôle & leurs effets
- Triggers automatiques en base
- Problèmes identifiés & incohérences
- Suggestions d'amélioration
1. Vue d'ensemble des entités
┌──────────────────────────────────────────────────────────────────┐
│ auth.users (Supabase) │
│ Identité technique (JWT) │
└───────────────────────────┬──────────────────────────────────────┘
│ trigger: handle_new_user
┌─────────────▼──────────────┐ ┌────────────────────┐
│ public.users │ │ public.admin_users │
│ userType: BUYER (défaut) │ │ role: ADMIN, etc. │
│ isVerified: false │ │ isActive: bool │
└────────────┬───────────────┘ └────────────────────┘
│ 1─1 (dealers.userId)
┌────────────▼───────────────┐
│ public.dealers │
│ requestedRole: DEALER| │
│ IMPORTER │
│ status: PENDING/APPROVED/ │
│ REJECTED/SUSPENDED │
│ isVerified: bool │
└────────────┬───────────────┘
│ trigger: notify_favorites_on_listing_changerr
┌────────────▼───────────────┐
│ users.userType change │
│ BUYER → DEALER|IMPORTER │
│ users.isVerified = true │
└────────────────────────────┘
2. Rôles utilisateurs (UserType)
Ces rôles vivent dans la colonne public.users.userType (enum PostgreSQL UserType).
2.1 BUYER (rôle par défaut)
| Attribut | Valeur |
|---|---|
| Attribué | Automatiquement à l'inscription via trigger handle_new_user |
isVerified | false au départ — passe true seulement après vérification email Supabase |
subscriptionType | FREE par défaut |
Ce qu'un BUYER peut faire :
| Table | Accès |
|---|---|
car_listings | ✅ Lire (annonces PUBLISHED + isActive) |
car_listings | ❌ Créer / Modifier (bloqué par policy car_listings_insert_own_not_buyer) |
dealers | ✅ Lire (concessionnaires APPROVED + isVerified) |
favorites | ✅ CRUD sur ses propres favoris |
messages | ✅ Envoyer / recevoir des messages |
reviews | ✅ Créer / modifier ses propres avis |
notifications | ✅ Voir/modifier ses propres notifications |
users | ✅ Voir / modifier son propre profil uniquement |
Ce qu'un BUYER ne peut PAS faire :
- Publier une annonce
- Accéder à un dashboard professionnel
- Gérer un inventaire
2.2 SELLER (rôle intermédiaire, partiellement défini)
⚠️ Incohérence détectée :
SELLERexiste dans les types TypeScript front-end (packages/shared/src/types.ts) mais n'est pas dans l'enum PostgreSQLUserTypede base. Il ne peut donc pas y être stocké en l'état.
Comportement attendu selon la policy RLS car_listings_insert_own_not_buyer :
- Tout utilisateur avec
userType != 'BUYER'peut créer et modifier des annonces. - SELLER devrait donc avoir ce privilege une fois ajouté à l'enum.
2.3 DEALER (rôle professionnel — dealer local)
| Attribut | Valeur |
|---|---|
| Attribué | Après approbation admin (dealers.status = APPROVED, requestedRole = DEALER) |
isVerified (users) | true — mis à jour par le trigger |
subscriptionType | Peut être DEALER |
Accès supplémentaires obtenus par rapport à BUYER :
| Capacité | Détail |
|---|---|
| ✅ Créer des annonces | Via policy car_listings_insert_own_not_buyer |
| ✅ Modifier ses annonces | Via policy car_listings_update_own_not_buyer |
| ✅ Gérer son profil dealer | Via policy Dealers can manage own profile |
✅ Dashboard /dashboard | Route protégée accessible (frontend middleware) |
| ✅ Gérer inventaire | Annonces groupées sous car_listings.dealerId |
| ✅ Ads propres | Via ads.dealerId |
2.4 IMPORTER (rôle professionnel — importateur)
| Attribut | Valeur |
|---|---|
| Attribué | Après approbation admin (requestedRole = IMPORTER) |
| Ajouté via migration | 20260220210112_adhesion_and_importers.sql |
Accès : identiques à DEALER au niveau RLS (aucune policy distincte actuellement).
⚠️ Les droits DEALER et IMPORTER sont actuellement identiques en base. La différence est sémantique (affichage, label) mais pas encore enforced par des règles distinctes.
2.5 RESELLER (déclaré mais non implémenté)
❌ Présent uniquement dans
packages/shared/src/types.ts(UserRole). Absent de l'enum PostgreSQL et sans policy RLS. À définir ou supprimer.
3. Rôles administrateurs (AdminRole)
Ces rôles vivent dans public.admin_users.role (enum AdminRole). Cette table est entièrement séparée de public.users — un admin n'est pas un utilisateur de l'app.
| Rôle | Permissions clés |
|---|---|
SUPER_ADMIN | Tout : users.CRUD, roles.manage, system.configure, analytics.full, dealers, listings, content, marketing |
ADMIN | users.read/update, analytics.view, dealers.manage, listings.manage, content.manage |
MANAGER | users.read, dealers.read/update, listings.read/update, analytics.view |
MARKETING_MANAGER | content.manage, marketing.manage, analytics.marketing, listings.read |
DEALER_MANAGER | dealers.manage, listings.manage, users.read |
CONTENT_MANAGER | content.manage, listings.read/update |
MODERATOR | listings.read/moderate, users.read |
Accès RLS des admins (toutes tables) :
Toute policy FOR ALL sur admin_users vérifie :
EXISTS (
SELECT 1 FROM admin_users
WHERE "supabaseId" = auth.uid()::text
AND "isActive" = true
)
Un admin actif peut donc lire et modifier toutes les lignes des tables protégées (users, dealers, car_listings, etc.).
4. Entités système non-utilisateurs
4.1 advertisers — Annonceurs publicitaires
- Entité externe (marques, entreprises)
status:ACTIVE / INACTIVE / SUSPENDED / PENDINGisVerified: booléen indépendant- Relie à
adsviaads.advertiserId - Aucun login direct — gérés par les admins via le panel
4.2 ads — Publicités
- Liées soit à un
advertiserId(externe), soit à undealerId(dealer boost) - Types :
BANNER, NATIVE, INTERSTITIAL, VIDEO, LISTING_BOOST, DEALER_SPOTLIGHT - Métriques :
clicks, impressions, webClicks, mobileClicks, etc.
4.3 dealers — Profils professionnels
- Table pivot entre
userset le statut professionnel - Contient
requestedRole(ce que l'utilisateur veut être) - Contient
documentUrl(RCCM, pièce d'identité) status:PENDING → APPROVED / REJECTED / SUSPENDEDisVerified(sur dealers) : indépendant deusers.isVerified
4.4 api_rate_limits
- Limiteur de requêtes pour le décodeur vPIC (VIN)
- Accessible seulement via
service_role(Edge Functions)
5. Flux d'authentification & vérification
5.1 Inscription d'un nouvel utilisateur
[User] S'inscrit via Supabase Auth (email/password ou OAuth)
│
▼
[Trigger] handle_new_user
→ INSERT INTO public.users
userType = 'BUYER'
isVerified = false
subscriptionType = 'FREE'
│
▼
[User] Reçoit email de vérification Supabase
(isVerified reste false côté applicatif jusqu'à vérification manuelle ou auto)
5.2 Demande de devenir Dealer ou Importateur
[BUYER] Remplit le formulaire /devenir-partenaire ou /devenir-pro
│
▼
[App] INSERT INTO public.dealers
userId = auth.uid()
requestedRole = 'DEALER' | 'IMPORTER'
documentUrl = <url RCCM / pièce>
status = 'PENDING'
isVerified = false
│
▼
[Admin Panel] Admin voit la demande (status = PENDING)
5.3 Approbation par l'admin
[Admin] Met à jour dealers.status = 'APPROVED'
│
▼
[Trigger] notify_favorites_on_listing_changerr
→ UPDATE public.users
userType = dealers.requestedRole ← BUYER → DEALER ou IMPORTER
isVerified = true
updatedAt = NOW()
→ INSERT INTO notifications
type = 'DEALER_APPROVED'
message = "Votre compte professionnel est approuvé 🎉"
url = '/dashboard'
5.4 Rejet par l'admin
[Admin] Met à jour dealers.status = 'REJECTED'
│
▼
[Trigger] notify_favorites_on_listing_changerr
→ Aucune modification de public.users (userType reste BUYER)
→ INSERT INTO notifications
type = 'DEALER_REJECTED'
url = '/support'
6. Matrice d'accès par rôle (RLS)
| Table | BUYER | SELLER | DEALER / IMPORTER | Admin actif |
|---|---|---|---|---|
users (propre profil) | R/U | R/U | R/U | R/U/D |
users (tous) | ❌ | ❌ | ❌ | ✅ |
car_listings (PUBLISHED) | R | R | R | R |
car_listings (INSERT) | ❌ | ✅ | ✅ | ✅ |
car_listings (UPDATE propre) | ❌ | ✅ | ✅ | ✅ |
car_listings (toutes) | ❌ | ❌ | ❌ | ✅ |
dealers (APPROVED) | R | R | R | R |
dealers (propre profil) | ❌ | ❌ | R/U | ✅ |
dealers (toutes) | ❌ | ❌ | ❌ | ✅ |
favorites | R/W propre | R/W propre | R/W propre | ✅ |
messages | R/W propre | R/W propre | R/W propre | ✅ |
reviews | R + W propre | R + W propre | R + W propre | ✅ |
notifications | R/U propre | R/U propre | R/U propre | ✅ |
media (published) | R | R | R | R |
media (propre listing) | ❌ | ✅ | ✅ | ✅ |
admin_users | ❌ | ❌ | ❌ | propre + SUPER_ADMIN |
ads | ❌ | ❌ | ❌ | ✅ |
advertisers | ❌ | ❌ | ❌ | ✅ |
system_config (public) | R si isPublic=true | idem | idem | ✅ |
api_rate_limits | ❌ | ❌ | ❌ | service_role only |
audit_logs | ❌ | ❌ | ❌ | SUPER_ADMIN |
Légende : R=lecture, U=mise à jour, W=écriture, ✅=accès complet, ❌=accès refusé
7. Changements de rôle & leurs effets
BUYER → DEALER
| Avant | Après |
|---|---|
| Peut seulement consulter les annonces | Peut créer, modifier, publier des annonces |
| Pas de profil professionnel | Profil dealer public sur le marketplace |
Pas d'accès /dashboard | Accès au dashboard complet |
isVerified = false | isVerified = true |
subscriptionType = FREE | Peut upgrader en DEALER |
| Notification reçue | DEALER_APPROVED + lien /dashboard |
BUYER → IMPORTER
Identique à BUYER → DEALER (mêmes effets techniques), mais :
- Label affiché : «importateur professionnel»
- Sémantiquement distinct pour l'affichage et les futures policies
DEALER → SUSPENDED (via dealers.status)
- L'admin peut mettre
dealers.status = SUSPENDED - Actuellement : aucun trigger ne rétrograde
users.userType - Le dealer garde donc ses droits de création d'annonces → ⚠️ À corriger
Admin → Désactivé (isActive = false)
is_admin()retournefalse- Toutes les policies admin échouent → perd tous les accès admin
- Son compte Supabase n'est pas supprimé
8. Triggers automatiques en base
| Trigger | Table | Événement | Effet |
|---|---|---|---|
handle_new_user | auth.users | INSERT | Crée le profil dans public.users avec userType=BUYER |
notify_favorites_on_listing_changerr | dealers | UPDATE (status) | Si APPROVED : promeut userType + isVerified. Si REJECTED : notifie. |
on_listing_change_notify_favorites | car_listings | UPDATE | Notifie les utilisateurs qui ont en favori : baisse de prix ou annonce vendue |
9. Problèmes identifiés & incohérences
🔴 Critique
| # | Problème | Impact |
|---|---|---|
| C1 | SELLER dans les types TS mais absent de l'enum PostgreSQL | Un userType='SELLER' ne peut pas être stocké en base → crashes/erreurs silencieuses |
| C2 | RESELLER dans les types TS, absent de la DB | Même problème |
| C3 | Suspension d'un dealer (dealers.status=SUSPENDED) ne rétrograde pas users.userType | Un dealer suspendu conserve ses droits (peut encore publier des annonces) |
🟠 Important
| # | Problème | Impact |
|---|---|---|
| I1 | Double isVerified : sur users ET sur dealers | La source de vérité est floue. Le trigger ne met à jour que users.isVerified, pas dealers.isVerified |
| I2 | Le middleware frontend ne vérifie que l'authentification, pas le rôle | Un BUYER peut accéder à /dashboard côté serveur si l'URL est connue |
| I3 | SubscriptionType (FREE/PREMIUM/DEALER) n'est enforced par aucune policy RLS | N'a aucun effet réel sur les accès actuellement |
| I4 | DEALER et IMPORTER partagent exactement les mêmes policies RLS | Impossible de donner des droits distincts (ex: importateur peut voir des catalogues exclusifs) |
🟡 Mineur
| # | Problème | Impact |
|---|---|---|
| M1 | dealers.userId est UNIQUE → un user ne peut avoir qu'un seul profil dealer | Si un dealer veut aussi être importateur, impossible actuellement |
| M2 | audit_logs n'a pas de policy RLS définie dans les fichiers vus | Potentiellement accessible ou inaccessible sans règle claire |
| M3 | notifications table : colonne message utilisée dans le trigger mais le schema définit content | Risque d'erreur à l'exécution du trigger de notification favoris |
10. Suggestions d'amélioration
10.1 Aligner les types TS avec la DB
Action : Soit ajouter SELLER et RESELLER à l'enum PostgreSQL, soit les retirer des types TypeScript.
-- Option A : ajouter à la DB
ALTER TYPE "public"."UserType" ADD VALUE IF NOT EXISTS 'SELLER';
ALTER TYPE "public"."UserType" ADD VALUE IF NOT EXISTS 'RESELLER';
// Option B : nettoyer les types TS
export type UserRole = "BUYER" | "DEALER" | "IMPORTER"
// supprimer SELLER et RESELLER jusqu'à définition métier claire
10.2 Trigger de suspension dealer
Ajouter dans notify_favorites_on_listing_changerr le cas SUSPENDED :
ELSIF NEW.status = 'SUSPENDED' THEN
UPDATE users
SET "userType" = 'BUYER', "updatedAt" = NOW()
WHERE id = NEW."userId";
-- + notification
END IF;
10.3 Middleware frontend avec vérification de rôle
Le middleware actuel ne fait que vérifier si l'utilisateur est connecté. Ajouter une vérification du rôle pour les routes professionnelles :
// middleware.ts
const dealerOnlyRoutes = ["/dashboard", "/creer-annonce", "/mes-annonces"]
const isDealerRoute = dealerOnlyRoutes.some(r => request.nextUrl.pathname.startsWith(r))
if (isDealerRoute && user) {
const { data: userProfile } = await supabase
.from("users")
.select("userType")
.eq("supabaseId", user.id)
.single()
if (!userProfile || userProfile.userType === "BUYER") {
return NextResponse.redirect(new URL("/devenir-partenaire", request.url))
}
}
10.4 Unifier la vérification (isVerified)
Actuellement isVerified existe sur users et sur dealers. Clarifier la sémantique :
| Colonne | Signification recommandée |
|---|---|
users.isVerified | L'identité de la personne est confirmée (email vérifié + éventuellement KYC) |
dealers.isVerified | Le profil professionnel (RCCM, documents) est validé par un admin |
Le trigger devrait mettre à jour les deux quand un dealer est approuvé :
-- Dans notify_favorites_on_listing_changerr, cas APPROVED
UPDATE dealers SET "isVerified" = true, "updatedAt" = NOW() WHERE id = NEW.id;
UPDATE users SET "userType" = NEW."requestedRole", "isVerified" = true, "updatedAt" = NOW() WHERE id = NEW."userId";
10.5 Enforcer SubscriptionType via RLS
Si PREMIUM donne accès à des fonctionnalités supplémentaires, le matérialiser en policy. Exemple :
-- Les annonces "featured" ne peuvent être créées que par des users DEALER/IMPORTER avec subscription DEALER
CREATE POLICY "only_premium_dealers_can_feature" ON car_listings
FOR UPDATE
WITH CHECK (
NOT NEW."isFeatured" OR EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid()::text
AND "userType" IN ('DEALER', 'IMPORTER')
AND "subscriptionType" = 'DEALER'
)
);
10.6 Séparer les droits DEALER et IMPORTER
Si les importateurs ont un accès différent (ex: catalogues constructeurs, importation en lot) :
-- Policy spécifique aux importateurs
CREATE POLICY "importers_can_access_import_catalog" ON import_catalog
FOR SELECT USING (
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid()::text AND "userType" = 'IMPORTER'
)
);
10.7 Architecture recommandée : tableau récapitulatif
┌────────────────────────────────────────────────────────────────────┐
│ COUCHES D'IDENTITÉ & D'ACCÈS │
├──────────────────┬─────────────────────────────────────────────────┤
│ Couche │ Responsabilité │
├──────────────────┼─────────────────────────────────────────────────┤
│ Supabase Auth │ Authentification (JWT, sessions, OAuth) │
│ public.users │ Profil applicatif + UserType (BUYER/DEALER/...) │
│ public.dealers │ Profil professionnel (statut, docs, demande) │
│ public.admin_users│ Identité admin + AdminRole (séparé des users) │
│ RLS Policies │ Contrôle d'accès en base (dernière ligne) │
│ Middleware Next.js│ Redirection routes protégées (côté serveur) │
│ Components React │ Affichage conditionnel selon rôle (côté client) │
└──────────────────┴─────────────────────────────────────────────────┘
10.8 Checklist prioritaire
- C1/C2 — Aligner
UserTypeDB ↔ types TypeScript (ajouter ou supprimer SELLER/RESELLER) - C3 — Ajouter le cas
SUSPENDEDdans le triggernotify_favorites_on_listing_changerr - I1 — Mettre à jour
dealers.isVerifieddans le trigger d'approbation - I2 — Ajouter vérification de rôle dans le middleware Next.js
- M3 — Corriger le nom de colonne dans le trigger
notify_favorites_on_listing_changer(contentet nonmessage) - I3 — Documenter ou enforcer
subscriptionTypevia policy si pertinent - I4 — Définir les droits distincts DEALER vs IMPORTER si les métiers divergent
Généré par introspection des migrations SQL, des policies RLS, des triggers et des types TypeScript du monorepo.