Skip to Content
Validation des Comptes Vendeur

Validation des Comptes Vendeur

Ce document décrit le flux d’auto-inscription des vendeurs et l’étape de validation par un administrateur qui la conditionne, avant qu’un compte SELLER puisse publier son premier produit. Modules concernés : apps/api/src/auth, apps/api/src/sellers, apps/api/src/users, et la page d’inscription du dashboard (apps/dashboard/app/auth).


1. Pourquoi cette validation ?

Avant cette fonctionnalité, un compte SELLER ne pouvait être créé que directement par un administrateur via POST /users (page “Utilisateurs” du dashboard) — il n’existait aucune inscription publique de vendeur. Le formulaire d’inscription du dashboard (apps/dashboard/app/auth/page.tsx) proposait déjà un sélecteur “Je suis Producteur / Je suis Acheteur” avec un champ caché role, mais le backend l’ignorait silencieusement : RegisterDto n’avait pas de champ role et auth.service.ts forçait UserRole.BUYER pour toute inscription publique.

Deux choses ont donc été construites ensemble :

  1. Le rôle choisi côté formulaire est désormais réellement transmis à l’API.
  2. Un compte SELLER auto-inscrit passe par un statut PENDING : il peut se connecter à son espace dashboard, mais ne peut pas publier de produit tant qu’un administrateur ne l’a pas approuvé.

[!IMPORTANT] ADMIN reste impossible à obtenir via l’inscription publique — toute valeur de role autre que SELLER retombe sur BUYER côté serveur (apps/api/src/auth/auth.service.ts), indépendamment de ce que le client envoie.


2. Modèle de données

Champs ajoutés sur User (migration 20260929120000_add_vendor_validation) :

ChampTypeDescription
vendorStatusVendorStatus? (PENDING, APPROVED, REJECTED)null pour BUYER/ADMIN. Statut de validation pour un SELLER.
vendorBusinessNameString?Nom de l’exploitation/activité, collecté à l’inscription.
vendorActivityTypeString?Type de production (maraîchage, élevage, apiculture…), collecté à l’inscription.
vendorDescriptionString?Description libre, facultative.
vendorRejectionReasonString?Motif de refus renseigné par l’admin si REJECTED.

[!NOTE] Backfill à la migration : les comptes SELLER déjà existants avant cette fonctionnalité (créés directement par un admin) ont été migrés en vendorStatus: APPROVED — ils sont considérés « vouchés » d’office puisque leur création n’a jamais pu passer par l’auto-inscription.


3. Deux chemins de création d’un compte SELLER

CheminEndpointvendorStatus initialQui peut le faire
Auto-inscriptionPOST /auth/register { role: 'SELLER', vendorBusinessName, vendorActivityType, vendorDescription? }PENDINGPublic (formulaire dashboard)
Création directePOST /users { role: 'SELLER', ... }APPROVED (par défaut, sauf si explicitement fourni)ADMIN uniquement

La logique de ce second cas est dans users.service.ts (create()) : un SELLER créé par un admin via la page “Utilisateurs” est considéré validé d’office, car un admin qui crée le compte lui-même vaut déjà “vouching”.


4. Flux d’inscription auto-vendeur

Le vendeur est donc connecté immédiatement (un token est retourné comme pour n’importe quelle inscription) — c’est volontaire : il peut explorer son espace dashboard tout de suite, seule la publication de produit reste bloquée.

[!NOTE] Anti-spam : AuthService maintient un registre en mémoire (Map<ip, timestamps[]>, fenêtre glissante d’une heure, 5 tentatives max) sur /auth/register. Ce n’est pas un Throttler NestJS packagé (aucune dépendance de ce type dans apps/api) — un choix volontairement simple, pas conçu pour résister à un DDoS, seulement à dissuader un bot qui spammerait de faux comptes vendeur dans la queue de validation admin. Comme SseService, cette hypothèse suppose un seul process API (pas de scaling horizontal).


5. Le bandeau vendeur en attente (dashboard)

Tant que vendorStatus !== 'APPROVED', le layout du dashboard (layout.tsx) affiche un bandeau permanent au-dessus du contenu, sur toutes les pages de l’espace vendeur :

  • PENDING : bandeau ambre, “compte en attente de validation, publication de produit bloquée”.
  • REJECTED : bandeau rouge, affiche le motif de refus (vendorRejectionReason) s’il est renseigné.

Le statut est lu via GET /auth/me (déjà utilisé par ailleurs pour détecter un compte bloqué isActive: false) — pas besoin d’un second appel réseau.


6. Blocage de la création de produit

Le garde-fou réel se situe côté API, dans products.service.ts (create()) :

const requester = await this.prisma.user.findUnique({ where: { id: sellerId }, select: { vendorStatus: true }, }); if (requester?.vendorStatus && requester.vendorStatus !== 'APPROVED') { throw new ForbiddenException('VENDOR_NOT_APPROVED'); }

Le message conventionnel VENDOR_NOT_APPROVED (même approche que ACCOUNT_BLOCKED pour un compte désactivé) est intercepté côté dashboard dans products/actions.ts pour afficher un message explicite plutôt qu’un “403 générique”.

[!IMPORTANT] Ce contrôle est la seule barrière fonctionnelle réellement bloquante du flux — le bandeau dashboard n’est qu’une indication UX. Toute autre tentative de création de produit (via un futur canal USSD par exemple, voir Enregistrement de produits par USSD) doit repasser par ProductsService.create() ou réimplémenter ce même contrôle.


7. Annuaire public des vendeurs

GET /sellers (public, alimente la carte des producteurs — voir Annuaire public des vendeurs & carte producteurs) ne renvoie que les vendeurs role: SELLER, isActive: true, et vendorStatus: APPROVED. Un vendeur PENDING ou REJECTED n’a de toute façon aucun produit (bloqué par le point précédent), mais ce filtre supplémentaire évite qu’il apparaisse dans l’annuaire avant validation.


8. Queue de validation admin

RBAC

EndpointMéthodeRôle requisDescription
/sellers/pendingGETADMINListe des vendeurs PENDING, triés par ancienneté (createdAt asc — les plus anciens en premier).
/sellers/:id/validatePATCHADMINApprouve ({ status: 'APPROVED' }) ou refuse ({ status: 'REJECTED', reason }) un vendeur PENDING.

Implémentation dans sellers.service.ts :

  • Le compte ciblé doit être role: SELLER et vendorStatus: PENDING — retraiter un compte déjà tranché renvoie 400 Bad Request ("Ce compte vendeur a déjà été traité.").
  • Un refus (REJECTED) exige un motif (reason) non vide — validé côté API, pas seulement côté formulaire.
  • Chaque décision est journalisée dans AuditLog ("Validation compte vendeur" / "Rejet compte vendeur", sévérité INFO/WARNING) — voir Journal d’audit.
  • Le vendeur concerné est notifié en temps réel via SseService.sendToUser(vendorId, 'vendor.status.decided', { status, reason }).

Interface admin (dashboard)

Page dédiée “Vendeurs à valider” (apps/dashboard/app/(dashboard)/vendor-validation/), avec badge de comptage dans la sidebar (mêmes conventions que “Grands Comptes” — voir Grands Comptes) :

  • Tableau des vendeurs en attente : nom d’exploitation, contact, type de production, description, date d’inscription.
  • Bouton Approuver (direct) et bouton Refuser (ouvre un champ de motif obligatoire avant confirmation).
  • Toast + rafraîchissement automatique de la liste (SSE) quand un nouveau vendeur s’inscrit (vendor.registration.created), géré par DashboardEventsListener.tsx.

9. Guide des Endpoints et Exemples de Payloads

Inscription vendeur (POST /auth/register)

{ "firstName": "Jean", "lastName": "Dupont", "username": "jeandupont", "email": "jean.dupont@example.com", "password": "motdepasse123", "phone": "+237 6xx xxx xxx", "role": "SELLER", "vendorBusinessName": "Ferme Dupont", "vendorActivityType": "Maraîchage", "vendorDescription": "Légumes de saison, vente directe depuis 2018." }

Réponse (201 Created) : identique à une inscription BUYER, un access_token — le statut PENDING n’est visible qu’via GET /auth/me, pas dans la réponse d’inscription.

Liste des vendeurs en attente (GET /sellers/pending, ADMIN)

[ { "id": "usr-uuid-seller", "firstName": "Jean", "lastName": "Dupont", "email": "jean.dupont@example.com", "phone": "+237 6xx xxx xxx", "vendorBusinessName": "Ferme Dupont", "vendorActivityType": "Maraîchage", "vendorDescription": "Légumes de saison, vente directe depuis 2018.", "createdAt": "2026-09-29T10:00:00.000Z", "country": { "name": "Cameroun" } } ]

Décision admin (PATCH /sellers/:id/validate)

Refus :

{ "status": "REJECTED", "reason": "Coordonnées incohérentes avec le pays déclaré, à revérifier." }

Approbation :

{ "status": "APPROVED" }

Tentative de création de produit par un vendeur non approuvé

POST /products → 403 Forbidden { "message": "VENDOR_NOT_APPROVED" }

10. Pistes d’évolution (non implémentées)

  • Pièce justificative (KYC léger) : upload d’un document (registre de commerce, pièce d’identité) en complément des champs déclaratifs actuels — nécessiterait un stockage de fichiers et une gestion RGPD dédiée, jugé disproportionné pour la première version.
  • Notification par SMS/email de la décision : aujourd’hui la seule notification est le SSE vendor.status.decided (donc seulement si le vendeur est connecté à ce moment-là) — voir Notifications SMS (Passerelle légère), qui liste déjà ce cas comme point d’appel candidat.
  • Second niveau de vérification automatique : aucune vérification d’email ou de téléphone n’est faite avant la mise en queue admin (pas de capacité SMTP/SMS dans ce repo) — la revue reste entièrement humaine.
Last updated on