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. AuthModulen'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 :
-
Deux
DIContainerdifférents. Celui de jobPipeline (container.set(key, value)/container.get(key),registerModule(name, module)) n'est PAS le même que celui attendu parAuthModuledu package (DIContainer.getInstance(config), registre de modules avec priorités,obtenirPrismaClient()...). Ce sont deux classes différentes du même nom — pas interchangeables. -
GestionnaireModulairede jobPipeline construit sonIHttpContexten castant directementreq/resd'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 :RevoquerTousTokensHandlerappellecontext.request.getHeader('Authorization'), qui n'existe pas sur un objetRequestExpress brut → plantera si câblé viaGestionnaireModulairesans adaptation. -
Format de token JWT différent. Le middleware
authMiddleware.tsde jobPipeline litpayload.suboupayload.id, et traitepayload.rolecomme une chaîne simple. Le package émet des tokens avecpayload.idUtilisateur(passub/id) etpayload.rolecomme 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.idresteraitundefined.
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¶
- Scaffolder l'app —
jobPipeline/FrontEnd/src/main.tsxn'existe pas encore (index.htmlle référence déjà). Créer un point d'entrée React standard avec<BrowserRouter>. - Résoudre les écarts de peer dependencies avant de pouvoir installer
proprement (sans
--legacy-peer-deps) : @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).lucide-react: jobPipeline a0.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.- Configurer :
import { configureAuthModule } from '@kanmaber/auth-frontend'; configureAuthModule({ apiBaseUrl: 'http://localhost:3000', security: { loginUrl: '/login', defaultRedirectAfterLogin: '/dashboard' }, }); - Monter les routes avec
<BrowserRouter>de jobPipeline (react-router-dom déjà aligné en version — vérifié le 2026-07-03), en utilisantLoginPage/RegisterPage/etc. exportées par le package.
Checklist finale¶
- [x] Variables d'environnement ajoutées à
.env(2026-07-03) - [x]
AuthPackageConfiguration.tscréé, monté dansapp.ts(2026-07-03) - [ ] Décision prise sur le middleware d'auth (dupliqué ou non)
- [x] Seed
ProfilUtilisateurexécuté — corrigé pourid: 'role_observateur'(2026-07-03, voir découverte Étape 4) - [x] Test
curlregister + connexion réussi (2026-07-03) - [ ]
@hookform/resolvers/lucide-reactalignés - [ ]
main.tsxcréé côté frontend,configureAuthModule()appelé - [ ] Test de connexion réel dans le navigateur