Functional prototype for an internal company portal using Node.js, TypeScript, Express, Zod, React Admin, Vite, and Docker Compose.
The backend follows Hexagonal Architecture:
back/src/domain: entities, invariants, lifecycle rules, domain exceptionsback/src/application: use cases, authorization policy, Zod API contractsback/src/ports: repository portsback/src/adapters: HTTP routes and PostgreSQL repository adaptersback/src/infrastructure: config, middleware, database bootstrap/migrations
Repositories are backed by PostgreSQL, included in Compose with a persistent volume. The database starts empty on a fresh environment — see back/README.md for how to create the first account.
postgres: PostgreSQL 15 with persistentpgdatabackend: Express API onhttp://localhost:3001/apifrontend-admin: React Admin onhttp://localhost:3000frontend-employee: Vite employee self-service onhttp://localhost:3002
docker-compose up --buildThen open:
- Admin/HR interface:
http://localhost:3000 - Employee interface:
http://localhost:3002 - API health:
http://localhost:3001/api/health
There is no seed data — the database starts empty. Register an account from the employee interface (it is always created with the EMPLOYEE role), then promote it to HR_ADMIN directly in the database:
UPDATE users SET role = 'HR_ADMIN' WHERE email = 'you@example.com';See back/README.md for details.
- Department code uniqueness and assignment checks
- Employee department assignment, valid email, and self-manager prevention
- Announcement lifecycle:
DRAFT -> PUBLISHED -> ARCHIVED - Feedback workflow:
SUBMITTED -> REVIEWED -> CLOSED - RBAC:
- HR Admin can manage departments, all employees, announcements, and feedback
- Department Admin can manage rows for their own department
- Employee can view published announcements and manage only their own feedback
- Field-level simulation: salary band is hidden from non-HR employee listings
Examples are provided in:
back/.env.examplefrontend-admin/.env.examplefrontend-employee/.env.example
For local non-Docker development, set VITE_API_URL=http://localhost:3001/api for both frontends.
Comparaison entre l'implémentation actuelle (branche dev) et le document Domain Model Specification (Nexus Advisory Group, v1, 12 Jun 2026).
- Directus : aucune référence trouvée dans tout le dépôt (code, config, docs). Rien à supprimer.
- Azure AD : entièrement retiré du backend, qui était le seul endroit où il restait des traces (le fournisseur d'authentification Azure lui-même avait déjà été supprimé lors d'une session précédente ; il ne restait que des champs et de la logique morte) :
User(entité domaine) : suppression du champazureId.UserRepository(port) : suppression de la méthode optionnellefindByAzureId.PostgresAuthUserRepository: suppression de la colonneazure_iddans les requêtes SQL et de la méthodefindByAzureId.schema.ts: suppression de la colonneazure_id TEXT UNIQUEde la tableusers.RegisterUseCase: suppression de la génération d'un fauxazureId(azure-${Date.now()}) et de la vérification d'unicité associée — ce mécanisme ne servait plus à rien depuis le retrait du vrai fournisseur Azure AD.PortalUseCases: suppression de la méthodelogin(email), du code mort qui simulait un « Mock Azure AD user » (jamais appelée par aucune route — le vrai login passe parLoginUseCase+AuthController).- Tests unitaires associés mis à jour en conséquence (
RegisterUseCase.test.ts,LoginUseCase.test.ts,ChangePasswordUseCase.test.ts,PostgresAuthUserRepository.test.ts,PortalUseCases.test.ts). README.md(racine) : la section « Demo Accounts » qui renvoyait vers un « mock Azure AD login » et des comptes de démonstration qui n'existent plus a été remplacée par une section « First Account » décrivant la vraie procédure (inscription + promotion SQL). La description de l'architecture a aussi été corrigée (elle décrivait encore des dépôts « in-memory », alors que tout est sur PostgreSQL depuis la session précédente).- Résultat vérifié :
tsc --noEmitpropre, suite de tests backend complète toujours au vert (220/220 tests, 22 suites, couverture ~90%).
Note : la colonne
azure_ida été retirée du fichierschema.ts, mais celui-ci utiliseCREATE TABLE IF NOT EXISTS(pas de vrai système de migration). Sur une base déjà existante et déployée, la colonne resterait physiquement présente (juste inutilisée) tant qu'unALTER TABLE users DROP COLUMN azure_id;n'est pas exécuté manuellement. Sans incidence sur un environnement neuf.
Le document fourni décrit un modèle métier « cible » (entités, invariants, cas d'usage). Le tableau ci-dessous confronte chaque élément du document à ce qui existe réellement dans back/src. Les écarts sont classés par sévérité :
- 🔴 Écart réel : la règle métier du document n'est pas respectée par le code (bug potentiel ou fonctionnalité manquante).
- 🟡 Écart mineur / nommage : le comportement est équivalent, seule la terminologie diffère.
- ✅ Conforme.
| Attendu (spec) | Présent dans le code | Statut |
|---|---|---|
id, email, firstName, lastName, role, isActive |
Présents, mais répartis sur deux classes différentes : User (identifiants de connexion : email/mot de passe/prénom/nom) et UserAccount (vue métier : displayName fusionné, departmentId, employeeId, active) |
🟡 Le modèle cible prévoit un seul agrégat User. Le code actuel duplique la notion en deux représentations qui doivent rester synchronisées manuellement (voir updateOwnProfile, qui met à jour les deux). Fonctionnellement correct mais plus fragile que prévu par le modèle. |
createdAt, updatedAt |
Colonnes présentes en base (users.created_at/updated_at) mais jamais exposées par les entités User/UserAccount |
🔴 Écart réel — l'invariant « toute modification auditable doit enregistrer l'utilisateur responsable » n'est pas vérifiable côté domaine puisqu'aucune entité ne porte ces champs. |
azureId |
Supprimé dans cette session | ℹ️ Écart voulu — le document mentionne Azure AD mais le système utilise désormais une authentification email/mot de passe locale. À mettre à jour dans une prochaine révision du document si Azure AD est officiellement abandonné. |
| Invariant : « un utilisateur ne peut pas changer son propre rôle » | updateOwnProfile ne touche jamais role |
✅ |
| Attendu (spec) | Présent dans le code | Statut |
|---|---|---|
employeeNumber (unique) |
Absent | 🔴 Champ requis par le document, jamais implémenté (ni dans l'entité, ni dans le schéma SQL). |
joinDate |
Absent | 🔴 Champ requis par le document, jamais implémenté. |
phone (optionnel) |
Absent | 🔴 Champ requis par le document, jamais implémenté. |
position |
Présent sous le nom jobTitle |
🟡 Nommage différent, même rôle. |
status (ACTIVE/INACTIVE/ON_LEAVE) |
Présent (ACTIVE/ON_LEAVE/INACTIVE) |
✅ |
userId (référence 1-1 vers User) |
Présent, mais généré artificiellement (user-${uuid()}) sans jamais créer la ligne User correspondante (voir §5) |
🔴 Écart réel — le lien Employee → User existe en apparence mais ne correspond à aucun compte utilisable. |
| — | Champs supplémentaires non prévus par le document : firstName, lastName, email dupliqués depuis User, managerId, salaryBand |
🟡 Le document ne prévoit pas de duplication d'identité sur Employee (l'identité devrait vivre uniquement sur User) ni de hiérarchie manager. Ce sont des ajouts fonctionnels raisonnables mais non documentés dans la spec fournie. |
| Attendu (spec) | Présent dans le code | Statut |
|---|---|---|
name, code (uniques) |
Présents, unicité vérifiée dans PortalUseCases |
✅ |
headOfDepartmentId |
Absent | 🔴 Le concept de « Head of Department » (attribut, invariant « au plus un par département », cas d'usage « Assign Head of Department ») n'existe nulle part dans le code : ni schéma, ni entité, ni cas d'usage. |
| Seuls les HR Admins créent/modifient | createDepartment/updateDepartment/deleteDepartment protégés par requireHr |
✅ |
| Attendu (spec) | Présent dans le code | Statut |
|---|---|---|
title, content, status, departmentId (optionnel), createdBy, publishedAt, archivedAt |
Tous présents (content → body, createdBy → authorId) |
🟡 Uniquement du nommage, aucun écart fonctionnel. |
| « Après publication, seul HR Admin peut modifier » | updateDraft() refuse toute modification dès que le statut n'est plus DRAFT, y compris pour un HR Admin |
🔴 Écart réel — le document autorise HR Admin à modifier une annonce publiée ; le code interdit cette modification à tout le monde une fois publiée. |
| « Delete Announcement » (HR Admin, cas exceptionnel) | Aucune route ni cas d'usage de suppression n'existe pour les annonces | 🔴 Fonctionnalité du document totalement absente. |
| Cycle de vie DRAFT → PUBLISHED → ARCHIVED, jamais de retour arrière | Respecté (publish() exige DRAFT, archive() exige PUBLISHED, aucune méthode de retour) |
✅ |
| Attendu (spec) | Présent dans le code | Statut |
|---|---|---|
title (optionnel), message (requis) |
Le code n'a pas de title distinct ; subject (obligatoire) en tient lieu |
🟡 Nommage/obligation différents mais couvre le besoin. |
employeeId et createdBy (deux attributs distincts) |
Un seul champ employeeId ; pour les acteurs sans profil employé, le code fabrique un identifiant de secours (employee-${actor.id}) |
🔴 Conséquence directe de l'absence du champ createdBy prévu par le document — la modélisation ne distingue pas « qui a soumis » de « quel profil employé est concerné ». |
Statuts : PENDING → REVIEWED → RESOLVED / CLOSED (4 états) |
Le code n'a que 3 états : SUBMITTED → REVIEWED → CLOSED — RESOLVED n'existe pas |
🔴 Écart réel sur la machine à états. |
| « Employés et Department Admins peuvent soumettre un feedback » | submitFeedback rejette explicitement tout acteur qui n'est pas EMPLOYEE (ForbiddenError) |
🔴 Contradiction directe avec le document — les Department Admins ne peuvent pas soumettre de feedback dans le code actuel, alors que la spec le demande explicitement (résumé des règles métier + tableau des cas d'usage 4.2). |
| « Review Feedback » réservé au HR Admin | reviewFeedback utilise requireHrOrDepartmentAdmin — un Department Admin peut aussi review |
🔴 Écart (le code est plus permissif que la spec). |
| « Delete Feedback » : le propriétaire ou un HR Admin | deleteFeedback n'autorise que le propriétaire (role === EMPLOYEE + vérification d'appartenance) ; un HR Admin ne peut jamais supprimer un feedback |
🔴 Fonctionnalité du document absente pour la partie HR Admin. |
| Cas d'usage (spec) | Statut réel |
|---|---|
| Create User (HR Admin crée un compte) | 🔴 Non implémenté. Le seul moyen de créer un compte est l'auto-inscription (/auth/register, toujours EMPLOYEE) ; un HR Admin n'a aucun moyen de provisionner un compte pour un tiers. |
| Assign Role | 🔴 Non implémenté en tant que fonctionnalité applicative — la seule voie actuelle est une commande SQL manuelle documentée dans back/README.md (décision assumée de cette session, mais ne répond pas formellement au cas d'usage attendu). |
| Deactivate User | 🔴 Non implémenté — aucune route ne permet de désactiver un compte. |
| Reactivate User | 🔴 Non implémenté, même constat. |
- « Chaque Employee est lié à exactement un User » : en pratique,
createEmployeegénère unuserIdfictif mais ne crée jamais la ligneUsercorrespondante. L'invariant est donc cassé silencieusement : un Employee créé par RH n'a pas de compte utilisable pour se connecter. Combiné au point précédent (pas de « Create User »), il n'existe aujourd'hui aucun parcours pour qu'un HR Admin fasse à la fois exister un Employee et un compte de connexion associé — seule l'auto-inscription de l'employé lui-même (sans lien vers un profil Employee) fonctionne de bout en bout. - « Seuls les HR Admins créent/modifient les Employee » : contredit par le code, qui autorise aussi les Department Admins (dans leur propre département) via
createEmployee/updateEmployee. Probablement un choix produit délibéré et raisonnable, mais à faire valider/documenter puisqu'il contredit le texte de la spec telle quelle. - Traçabilité (
createdBy/updatedBy) : partiellement assurée (Announcement aauthorId, Feedback areviewerId), mais Employee et Department n'ont ni l'un ni l'autre — seulement des timestamps.
- Le cycle de vie des Announcements (DRAFT/PUBLISHED/ARCHIVED, transitions à sens unique, contenu minimum requis) est fidèle à la spec, à l'exception du point sur l'édition post-publication par HR Admin et de la suppression absente.
- Le scoping par département (Department Admin ne voit/gère que son département) est cohérent et appliqué systématiquement via
AuthorizationService, aussi bien pour les employés, les annonces que le feedback. - Les invariants d'unicité de département (nom/code) et d'auto-management interdit (
INVALID_MANAGER) sont bien couverts. - La règle « un utilisateur ne peut pas changer son propre rôle » et « utilisateur inactif ne peut rien faire » sont correctement appliquées à l'exécution.
- La couverture de tests est solide sur tout ce qui existe (~90% backend, 220 tests après le nettoyage Azure AD), ce qui limite le risque de régression lors des corrections des écarts listés ci-dessus.
Fonctionnalités du document totalement absentes du code :
- Head of Department (attribut, invariant, cas d'usage).
- Delete Announcement.
- Delete Feedback par un HR Admin.
- Create User / Assign Role / Deactivate User / Reactivate User (gestion des comptes par un HR Admin).
- Champs Employee :
employeeNumber,joinDate,phone. - Statut Feedback
RESOLVED. - Champ Feedback
createdBydistinct d'employeeId.
Contradictions directes avec une règle métier explicite du document :
- Les Department Admins ne peuvent pas soumettre de feedback (la spec dit qu'ils le peuvent).
- Une annonce publiée ne peut plus être modifiée par personne, y compris HR Admin (la spec dit que HR Admin le peut).
- Le lien Employee ↔ User n'est pas réellement établi à la création d'un Employee.
Le code est plus permissif que la spec (à valider si voulu) :
- Les Department Admins peuvent modifier des Employee (spec réserve ça à HR Admin).
- Les Department Admins peuvent review un Feedback (spec réserve ça à HR Admin).
Aucune de ces actions n'a été effectuée automatiquement — ce rapport est une base de décision. Les points de la section « Contradictions directes » sont les plus importants à trancher rapidement, car ce sont des règles métier écrites noir sur blanc qui ne sont pas respectées (en particulier : Department Admin qui ne peut pas soumettre de feedback, et l'absence de compte utilisateur réel derrière chaque Employee créé par RH). Les autres points sont des fonctionnalités manquantes qu'il faut simplement prioriser selon les besoins réels du produit.