Aller au contenu

Procédure détaillée — câbler @kanmaber/auth-backend et auth-frontend dans jobPipeline

Date : 2026-07-03

État actuel (résumé)

  • Packages installés (node_modules/@kanmaber/) via tarball, versions figées.
  • Schéma Prisma fusionné et poussé en base (jobpipeline_db) — 13 tables auth existent réellement.
  • AuthModule n'est appelé nulle part dans le code de jobPipeline — seul un script de smoke test l'instancie isolément.
  • Ce document couvre tout ce qui reste, avec le code exact à écrire.

Découverte préalable importante : jobPipeline a sa propre architecture DI/HTTP, incompatible telle quelle

En inspectant le code de jobPipeline (src/shared/infrastructure/di/DIContainer.ts, GestionnaireModulaire.ts, middleware/authMiddleware.ts), j'ai trouvé trois incompatibilités concrètes avec le package, à connaître avant de commencer :

  1. Deux DIContainer différents. Celui de jobPipeline (container.set(key, value) / container.get(key), registerModule(name, module)) n'est PAS le même que celui attendu par AuthModule du package (DIContainer.getInstance(config), registre de modules avec priorités, obtenirPrismaClient()...). Ce sont deux classes différentes du même nom — pas interchangeables.

  2. GestionnaireModulaire de jobPipeline construit son IHttpContext en castant directement req/res d'Express, sans implémenter les méthodes de confort de l'interface (getHeader(), getBearerToken(), getAllData(), etc.). La quasi-totalité des handlers du package n'en ont pas besoin (ils utilisent des méthodes qu'Express fournit nativement : .status().json(), .headers, .method...), sauf un : RevoquerTousTokensHandler appelle context.request.getHeader('Authorization'), qui n'existe pas sur un objet Request Express brut → plantera si câblé via GestionnaireModulaire sans adaptation.

  3. Format de token JWT différent. Le middleware authMiddleware.ts de jobPipeline lit payload.sub ou payload.id, et traite payload.role comme une chaîne simple. Le package émet des tokens avec payload.idUtilisateur (pas sub/id) et payload.role comme un objet ({ idRole, nom, permissions[] }). Un token émis par le package ne sera pas correctement interprété par le middleware existant de jobPipeline tel quel — req.utilisateur.id resterait undefined.

Conséquence : plutôt que de forcer l'intégration dans GestionnaireModulaire (ce qui demanderait de corriger son bridge IHttpContext et son format de token), la méthode la plus sûre est d'utiliser le propre bridge du package (AuthModuleController, déjà complet) avec un routeur Express dédié, et un container DI dédié — en parallèle du système de jobPipeline, sans le modifier. C'est l'Option A ci-dessous, recommandée.


Étape 1 — Variables d'environnement

Ajouter dans jobPipeline/BackEnd/.env (ne pas commiter) :

JWT_ACCESS_SECRET=<une valeur secrète longue>
JWT_REFRESH_SECRET=<une autre valeur secrète longue>
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d
TOTP_ENCRYPTION_KEY=<32 bytes hex — ex: openssl rand -hex 32>
BCRYPT_ROUNDS=12
SMTP_HOST=...
SMTP_PORT=587
SMTP_USER=...
SMTP_PASS=...
SMTP_SECURE=true
EMAIL_FROM=noreply@jobpipeline.example
EMAIL_FROM_NAME=JobPipeline
APP_NAME=JobPipeline
APP_URL=http://localhost:5173

JWT_SECRET (utilisé par l'authMiddleware existant de jobPipeline) reste séparé — voir Étape 4 pour la raison.

Redis est optionnel (fallback automatique en cache mémoire si absent).


Étape 2 — Container DI dédié + montage des routes (Option A recommandée)

Créer un nouveau fichier jobPipeline/BackEnd/src/shared/infrastructure/config/AuthPackageConfiguration.ts :

import {
  AuthModule,
  AuthModuleController,
  authModuleRoutes,
  DIContainer as AuthDIContainer,
} from '@kanmaber/auth-backend';

export async function initialiserAuthPackage() {
  const authContainer = AuthDIContainer.getInstance({ enableLogging: true });
  const authModule = new AuthModule(authContainer);
  await authModule.initialize(authContainer);

  const c = authModule.controllers;
  const authModuleController = new AuthModuleController({
    auth: {
      connexionController: c.connexion,
      inscriptionController: c.inscription,
      deconnexionController: c.deconnexion,
      refreshTokenController: c.refreshToken,
      verifierEmailController: c.verifierEmail,
      verifierNouvelEmailController: c.verifierNouvelEmail,
      changerEmailController: c.changerEmail,
      renvoyerEmailActivationController: c.renvoyerEmailActivation,
    },
    twoFactor: {
      activer2FAController: c.activer2FA,
      verifier2FAController: c.verifier2FA,
      generer2FASecretController: c.generer2FASecret,
      desactiver2FAController: c.desactiver2FA,
      genererBackupCodesController: c.genererBackupCodes,
      verifierBackupCodeController: c.verifierBackupCode,
      revoquerBackupCodesController: c.revoquerBackupCodes,
      envoyerCode2FAController: c.envoyerCode2FA,
    },
    password: {
      changerMotDePasseController: c.changerMotDePasse,
      demanderResetPasswordController: c.demanderResetPassword,
      verifierTokenResetController: c.verifierTokenReset,
      resetPasswordController: c.resetPassword,
    },
    sessions: {
      listerSessionsActivesController: c.listerSessionsActives,
      revoquerSessionController: c.revoquerSession,
      revoquerToutesSessionsController: c.revoquerToutesSessions,
      obtenirHistoriqueSessionController: c.obtenirHistoriqueSession,
    },
    tokens: {
      validerAccessTokenController: c.validerAccessToken,
      revoquerTokenController: c.revoquerToken,
      revoquerTousTokensController: c.revoquerTousTokens,
    },
    profile: {
      obtenirProfilController: c.obtenirProfil,
      modifierProfilController: c.modifierProfil,
      obtenirParametresController: c.obtenirParametres,
      supprimerCompteController: c.supprimerCompte,
      obtenirHistoriqueController: c.obtenirHistorique,
    },
    security: {
      bloquerIPController: c.bloquerIP,
      debloquerIPController: c.debloquerIP,
      obtenirLogsSecuriteController: c.obtenirLogsSecurite,
      detecterConnexionsSuspectesController: c.detecterConnexionsSuspectes,
    },
  });

  return { authModuleController, routes: authModuleRoutes };
}

(Mapping identique à celui déjà vérifié fonctionnel par scripts/smoke-test-auth-package.mjs.)

Dans jobPipeline/BackEnd/src/app.ts, ajouter le montage — avant le montage de GestionnaireModulaire existant (ou après, l'ordre n'a pas d'importance ici car préfixes différents) :

import { Router } from 'express';
import { initialiserAuthPackage } from '@/shared/infrastructure/config/AuthPackageConfiguration';

export async function createApp(container: DIContainer): Promise<Application> {
  const app: Application = express();
  // ... middlewares existants (helmet, cors, etc.) ...

  const { authModuleController, routes } = await initialiserAuthPackage();
  const authRouter = Router();
  for (const route of routes.routes) {
    const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch';
    const handler = (authModuleController as unknown as Record<string, express.RequestHandler>)[route.handler];
    authRouter[method](route.path, handler);
  }
  app.use(routes.prefix, authRouter); // prefix = '/api/auth'

  // ... reste de app.ts (GestionnaireModulaire, /health, 404, erreurs) ...
}

Note sur isPublic : authModuleRoutes marque certaines routes comme publiques (connexion, register, refresh...) et d'autres protégées. AuthModuleController ne vérifie PAS lui-même isPublic — c'est normalement le rôle du gestionnaire de routes. Pour l'instant, ce montage minimal ne bloque aucune route (elles vérifient l'authentification chacune à leur niveau via context.getUserId(), qui lève une erreur si pas authentifié). C'est fonctionnellement correct mais moins strict qu'un vrai middleware amont — acceptable pour une première mise en route, à durcir plus tard si besoin (ajouter un middleware qui vérifie route.isPublic et exige un Authorization header sinon, avant d'appeler le handler).


Étape 3 — Adapter (ou dupliquer) le middleware d'authentification

Le authMiddleware.ts existant de jobPipeline ne lira pas correctement les tokens émis par le package (payload différent — voir découverte n°3 ci-dessus). Si jobPipeline doit un jour protéger SES PROPRES routes métier (candidatures, CV...) avec les tokens du package, créer un second middleware plutôt que modifier l'existant (qui pourrait servir à autre chose) :

// jobPipeline/BackEnd/src/shared/infrastructure/http/middleware/authPackageMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export function authPackageMiddleware(req: Request, res: Response, next: NextFunction): void {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    res.status(401).json({ succes: false, code: 'NON_AUTORISE', message: "Token manquant" });
    return;
  }
  try {
    const payload = jwt.verify(authHeader.slice(7), process.env.JWT_ACCESS_SECRET!) as any;
    (req as any).utilisateur = {
      id: payload.idUtilisateur,           // <- pas payload.sub/id
      email: payload.email,
      role: payload.role?.nom ?? 'user',   // <- payload.role est un objet
      permissions: payload.role?.permissions ?? [],
    };
    next();
  } catch {
    res.status(401).json({ succes: false, code: 'TOKEN_INVALIDE', message: 'Token invalide ou expiré' });
  }
}

Ne pas utiliser ce nouveau middleware sur les routes /api/auth/* elles-mêmes (le package gère sa propre validation en interne) — seulement si d'autres routes métier de jobPipeline doivent vérifier un utilisateur connecté via ce système.


Étape 4 — Seed d'un ProfilUtilisateur

Utilisateur.profilId est obligatoire. Créer un script jobPipeline/BackEnd/prisma/seeds/seed.ts (référencé par npm run db:seed mais actuellement inexistant) :

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  await prisma.profilUtilisateur.upsert({
    where: { code: 'UTILISATEUR' },
    update: {},
    create: {
      code: 'UTILISATEUR',
      libelle: 'Utilisateur standard',
      description: 'Profil par défaut — chercheur d\'emploi',
    },
  });
  console.log('Profil UTILISATEUR créé/vérifié.');
}

main()
  .catch((e) => { console.error(e); process.exit(1); })
  .finally(() => prisma.$disconnect());

Puis : npm run db:seed --workspace=BackEnd

Découverte (test réel du 2026-07-03) — le seed ci-dessus est INCORRECT tel quel. InscrireUtilisateurUseCase (package @kanmaber/auth-backend, application/usecases/InscrireUtilisateurUseCase.js) hardcode idRole: 'role_observateur' et cette valeur est utilisée directement comme profilId par UtilisateurRepositoryPrisma.creerAvecCodeVerification() (profilId: utilisateurData.idRole) — sans lookup par code. Un seed qui upsert sur code: 'UTILISATEUR' génère un id cuid aléatoire, qui ne vaut jamais 'role_observateur' → l'inscription échoue avec Foreign key constraint violated: utilisateurs_profilId_fkey.

Seed corrigé — fixer explicitement l'id à 'role_observateur' :

await prisma.profilUtilisateur.upsert({
  where: { id: 'role_observateur' },
  update: {},
  create: {
    id: 'role_observateur',
    code: 'UTILISATEUR',
    libelle: 'Utilisateur standard',
    description: 'Profil par défaut — chercheur d\'emploi',
  },
});

Si un ProfilUtilisateur a déjà été créé avec l'ancien seed (id aléatoire, code: 'UTILISATEUR'), le supprimer d'abord (code est @unique, donc le nouveau upsert par id entrerait en conflit sinon) :

await prisma.profilUtilisateur.delete({ where: { id: '<ancien-id-cuid>' } });

Autres écarts constatés lors du test réel (curl), à connaître avant de tester soi-même : - InscrireUtilisateurSchema (Zod, package) exige en plus confirmationMotDePasse (doit être égal à motDePasse) et accepteCGU: true — absents de l'exemple curl de l'Étape 5 ci-dessous à l'origine, sinon VALIDATION_ECHEC. - motDePasse : min. 12 caractères, au moins 1 majuscule, 1 minuscule, 1 chiffre, 1 caractère spécial. - La connexion échoue avec EMAIL_NON_VERIFIE tant que emailVerifie n'est pas true en base — comportement normal, pas un bug. En l'absence de SMTP fonctionnel pour recevoir le code, on peut le forcer manuellement pour tester : prisma.utilisateur.update({ where: { email }, data: { emailVerifie: true } }). - Échec d'envoi d'email de vérification possible (550 SPAM ou autre rejet SMTP) sans bloquer l'inscription — InscrireUtilisateurUseCase capture l'erreur et retourne codeVerificationEnvoye: false avec un warning, l'utilisateur est bien créé. Si l'email ne part jamais, vérifier la config SMTP (.env) indépendamment du câblage.


Étape 5 — Test end-to-end réel

Une fois les étapes 1-4 faites, démarrer le serveur (npm run start:dev dans jobPipeline/BackEnd) et tester :

# Inscription — payload complet (voir écarts constatés à l'Étape 4)
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","motDePasse":"MotDePasse123!","confirmationMotDePasse":"MotDePasse123!","accepteCGU":true,"nom":"Test","prenom":"User"}'

# Connexion (nécessite emailVerifie=true en base — cf. note Étape 4)
curl -X POST http://localhost:3000/api/auth/connexion \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","motDePasse":"MotDePasse123!"}'

C'est le seul test qui prouve vraiment que le câblage fonctionne — tout ce qui précède (smoke tests) ne vérifie que l'assemblage des objets, jamais une vraie requête HTTP de bout en bout.

Résultat réel (2026-07-03) : test complet effectué avec succès après correction du seed (Étape 4) — inscription réussie, connexion réussie (après avoir forcé emailVerifie=true manuellement faute de SMTP fonctionnel), JWT émis avec le format attendu (idUtilisateur + role: { idRole, nom, permissions[] }), conforme à la découverte n°3 en tête de document. Utilisateurs de test nettoyés après coup (penser à supprimer aussi codeVerification/session/ tentativeConnexion/auditLog liés avant utilisateur.delete, pas de cascade en base).


Étape 6 — Frontend

  1. Scaffolder l'appjobPipeline/FrontEnd/src/main.tsx n'existe pas encore (index.html le référence déjà). Créer un point d'entrée React standard avec <BrowserRouter>.
  2. Résoudre les écarts de peer dependencies avant de pouvoir installer proprement (sans --legacy-peer-deps) :
  3. @hookform/resolvers : jobPipeline a ^5.0.1, le package exige ^3.3.0. Soit downgrader côté jobPipeline (risqué si d'autres formulaires du projet dépendent déjà de l'API v5), soit vérifier la compatibilité v5 des formulaires du package (LoginForm, ChangePasswordForm...) et élargir le peer range du package en conséquence (même démarche que pour zustand).
  4. lucide-react : jobPipeline a 0.488.0, le package exige ^1.7.0 (changement d'API entre ces versions — noms d'icônes, imports). À vérifier composant par composant avant de trancher.
  5. Configurer :
    import { configureAuthModule } from '@kanmaber/auth-frontend';
    configureAuthModule({
      apiBaseUrl: 'http://localhost:3000',
      security: { loginUrl: '/login', defaultRedirectAfterLogin: '/dashboard' },
    });
    
  6. Monter les routes avec <BrowserRouter> de jobPipeline (react-router-dom déjà aligné en version — vérifié le 2026-07-03), en utilisant LoginPage/RegisterPage/etc. exportées par le package.

Checklist finale

  • [x] Variables d'environnement ajoutées à .env (2026-07-03)
  • [x] AuthPackageConfiguration.ts créé, monté dans app.ts (2026-07-03)
  • [ ] Décision prise sur le middleware d'auth (dupliqué ou non)
  • [x] Seed ProfilUtilisateur exécuté — corrigé pour id: 'role_observateur' (2026-07-03, voir découverte Étape 4)
  • [x] Test curl register + connexion réussi (2026-07-03)
  • [ ] @hookform/resolvers / lucide-react alignés
  • [ ] main.tsx créé côté frontend, configureAuthModule() appelé
  • [ ] Test de connexion réel dans le navigateur