Aller au contenu principal

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

  1. Vue d'ensemble des entités
  2. Rôles utilisateurs (UserType)
  3. Rôles administrateurs (AdminRole)
  4. Entités système non-utilisateurs
  5. Flux d'authentification & vérification
  6. Matrice d'accès par rôle (RLS)
  7. Changements de rôle & leurs effets
  8. Triggers automatiques en base
  9. Problèmes identifiés & incohérences
  10. 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)

AttributValeur
AttribuéAutomatiquement à l'inscription via trigger handle_new_user
isVerifiedfalse au départ — passe true seulement après vérification email Supabase
subscriptionTypeFREE par défaut

Ce qu'un BUYER peut faire :

TableAccè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 : SELLER existe dans les types TypeScript front-end (packages/shared/src/types.ts) mais n'est pas dans l'enum PostgreSQL UserType de 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)

AttributValeur
AttribuéAprès approbation admin (dealers.status = APPROVED, requestedRole = DEALER)
isVerified (users)true — mis à jour par le trigger
subscriptionTypePeut être DEALER

Accès supplémentaires obtenus par rapport à BUYER :

CapacitéDétail
✅ Créer des annoncesVia policy car_listings_insert_own_not_buyer
✅ Modifier ses annoncesVia policy car_listings_update_own_not_buyer
✅ Gérer son profil dealerVia policy Dealers can manage own profile
✅ Dashboard /dashboardRoute protégée accessible (frontend middleware)
✅ Gérer inventaireAnnonces groupées sous car_listings.dealerId
✅ Ads propresVia ads.dealerId

2.4 IMPORTER (rôle professionnel — importateur)

AttributValeur
AttribuéAprès approbation admin (requestedRole = IMPORTER)
Ajouté via migration20260220210112_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ôlePermissions clés
SUPER_ADMINTout : users.CRUD, roles.manage, system.configure, analytics.full, dealers, listings, content, marketing
ADMINusers.read/update, analytics.view, dealers.manage, listings.manage, content.manage
MANAGERusers.read, dealers.read/update, listings.read/update, analytics.view
MARKETING_MANAGERcontent.manage, marketing.manage, analytics.marketing, listings.read
DEALER_MANAGERdealers.manage, listings.manage, users.read
CONTENT_MANAGERcontent.manage, listings.read/update
MODERATORlistings.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 / PENDING
  • isVerified : booléen indépendant
  • Relie à ads via ads.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 à un dealerId (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 users et le statut professionnel
  • Contient requestedRole (ce que l'utilisateur veut être)
  • Contient documentUrl (RCCM, pièce d'identité)
  • status : PENDING → APPROVED / REJECTED / SUSPENDED
  • isVerified (sur dealers) : indépendant de users.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)

TableBUYERSELLERDEALER / IMPORTERAdmin actif
users (propre profil)R/UR/UR/UR/U/D
users (tous)
car_listings (PUBLISHED)RRRR
car_listings (INSERT)
car_listings (UPDATE propre)
car_listings (toutes)
dealers (APPROVED)RRRR
dealers (propre profil)R/U
dealers (toutes)
favoritesR/W propreR/W propreR/W propre
messagesR/W propreR/W propreR/W propre
reviewsR + W propreR + W propreR + W propre
notificationsR/U propreR/U propreR/U propre
media (published)RRRR
media (propre listing)
admin_userspropre + SUPER_ADMIN
ads
advertisers
system_config (public)R si isPublic=trueidemidem
api_rate_limitsservice_role only
audit_logsSUPER_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

AvantAprès
Peut seulement consulter les annoncesPeut créer, modifier, publier des annonces
Pas de profil professionnelProfil dealer public sur le marketplace
Pas d'accès /dashboardAccès au dashboard complet
isVerified = falseisVerified = true
subscriptionType = FREEPeut upgrader en DEALER
Notification reçueDEALER_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() retourne false
  • Toutes les policies admin échouent → perd tous les accès admin
  • Son compte Supabase n'est pas supprimé

8. Triggers automatiques en base

TriggerTableÉvénementEffet
handle_new_userauth.usersINSERTCrée le profil dans public.users avec userType=BUYER
notify_favorites_on_listing_changerrdealersUPDATE (status)Si APPROVED : promeut userType + isVerified. Si REJECTED : notifie.
on_listing_change_notify_favoritescar_listingsUPDATENotifie les utilisateurs qui ont en favori : baisse de prix ou annonce vendue

9. Problèmes identifiés & incohérences

🔴 Critique

#ProblèmeImpact
C1SELLER dans les types TS mais absent de l'enum PostgreSQLUn userType='SELLER' ne peut pas être stocké en base → crashes/erreurs silencieuses
C2RESELLER dans les types TS, absent de la DBMême problème
C3Suspension d'un dealer (dealers.status=SUSPENDED) ne rétrograde pas users.userTypeUn dealer suspendu conserve ses droits (peut encore publier des annonces)

🟠 Important

#ProblèmeImpact
I1Double isVerified : sur users ET sur dealersLa source de vérité est floue. Le trigger ne met à jour que users.isVerified, pas dealers.isVerified
I2Le middleware frontend ne vérifie que l'authentification, pas le rôleUn BUYER peut accéder à /dashboard côté serveur si l'URL est connue
I3SubscriptionType (FREE/PREMIUM/DEALER) n'est enforced par aucune policy RLSN'a aucun effet réel sur les accès actuellement
I4DEALER et IMPORTER partagent exactement les mêmes policies RLSImpossible de donner des droits distincts (ex: importateur peut voir des catalogues exclusifs)

🟡 Mineur

#ProblèmeImpact
M1dealers.userId est UNIQUE → un user ne peut avoir qu'un seul profil dealerSi un dealer veut aussi être importateur, impossible actuellement
M2audit_logs n'a pas de policy RLS définie dans les fichiers vusPotentiellement accessible ou inaccessible sans règle claire
M3notifications table : colonne message utilisée dans le trigger mais le schema définit contentRisque 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 :

ColonneSignification recommandée
users.isVerifiedL'identité de la personne est confirmée (email vérifié + éventuellement KYC)
dealers.isVerifiedLe 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 UserType DB ↔ types TypeScript (ajouter ou supprimer SELLER/RESELLER)
  • C3 — Ajouter le cas SUSPENDED dans le trigger notify_favorites_on_listing_changerr
  • I1 — Mettre à jour dealers.isVerified dans 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 (content et non message)
  • I3 — Documenter ou enforcer subscriptionType via 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.