Développeurs et intégrateurs

Parqueo — documentation technique

Référence d'architecture et d'implémentation. Public visé : les personnes qui reprennent, modifient ou auditent le code.


1. Vue d'ensemble#

Parqueo est un logiciel ITSM auto-hébergé : ticketing, inventaire de parc, catalogue de demandes, base de connaissances et automatisation par workflows visuels. Il se déploie en deux processus :

Navigateur
    │
    │  HTTP (même origine en production)
    ▼
Reverse proxy ────────┬──────────────────────────────┐
                      │ /                            │ /api
                      ▼                              ▼
             client/dist (SPA statique)      API Express (port 4000)
                                                     │
                        ┌────────────────────────────┼───────────────┐
                        ▼                            ▼               ▼
                   PostgreSQL                 server/uploads/    SMTP / IMAP
                    (Prisma)                  (pièces jointes)

Le serveur ne rend aucune page : c'est une API JSON, plus une seule exception (la page HTML de l'enquête de satisfaction, GET /api/tickets/satisfaction). Le client est une SPA React qui appelle /api en relatif — ce qui suppose que le proxy serve les deux sur la même origine (voir la documentation d'installation).

Trois processus de fond démarrent avec l'API, dans app.listen : la clôture automatique des tickets résolus, le collecteur IMAP, et — au fil des requêtes — le moteur de workflows.


2. Pile technique#

Couche Choix Version
Runtime Node.js (modules ESM, "type": "module") 22.x
API Express ^4.21
ORM Prisma Client ^5.20
Base PostgreSQL 14+ (testé sur 18)
Auth jsonwebtoken (HS256) + bcrypt ^9.0 / ^6.0
Sécurité HTTP helmet, cors, express-rate-limit ^8.3 / ^2.8 / ^8.6
Email sortant nodemailer ^9.0
Email entrant imapflow + mailparser ^1.4 / ^3.9
Upload multer (stockage disque) ^2.2
Front React + React Router ^19.2 / ^7.18
Build front Vite ^8.1
Styles Tailwind CSS (plugin Vite) ^4.3
Lint front oxlint ^1.71

Aucune dépendance de framework côté API (pas de NestJS, pas de couche de validation type Zod) : les routeurs Express valident à la main, ce qui garde le volume de code bas au prix d'une validation à relire route par route.

Aucun client OIDC tiers non plus : le SSO Entra ID est implémenté directement avec fetch + crypto natif + jsonwebtoken (voir §5.3).


3. Organisation du dépôt#

server/
  prisma/
    schema.prisma                  modèle de données (source de vérité)
    migrations/                    11 migrations SQL versionnées
    seed.js                        jeu minimal : 1 équipe, 1 catégorie, 1 admin
    seed-demo.js                   jeu de démonstration (⚠ voir §14)
    migrate-rules-to-workflows.js  script ponctuel de migration historique
    backfill-workflow-edges.js     script ponctuel de migration historique
  src/
    index.js                       montage Express, middlewares, démarrage
    lib/
      prisma.js                    instance PrismaClient partagée
      appSettings.js               paramètres globaux + cache 10 s
      visibility.js                règle de visibilité des tickets (pure)
      workflowUtils.js             helpers du moteur (purs)
      inbound.js                   parsing des emails entrants (pur)
      labels.js                    libellés FR pour les emails
    middleware/
      auth.js                      vérification du JWT → req.user
      roles.js                     requireRole(...)
    routes/                        auth, tickets, assets, users, workflows,
                                   forms, kb, settings, refs
    services/
      workflowEngine.js            machine à états des workflows
      mailer.js                    notifications sortantes
      mailbox.js                   collecteur IMAP
      oidc.js                      SSO Microsoft Entra ID
      autoclose.js                 clôture automatique horaire
  uploads/                         pièces jointes (hors Git)

client/
  src/
    App.jsx                        routes React Router
    api/client.js                  wrapper fetch + jeton
    context/                       AuthContext, SettingsContext
    components/                    Layout, Brand, DataTable, WorkflowCanvas…
    pages/                         13 pages
    lib/
      dashboard.js                 métriques, catalogue de widgets, layouts
      workflowBlocks.js            catalogue de blocs du canvas
      labels.js                    libellés et couleurs
      csv.js                       parseur CSV de l'import d'utilisateurs

Les modules lib/ du serveur sont volontairement purs (aucun accès base, aucun I/O) : ce sont eux que couvrent les tests unitaires.


4. Modèle de données#

14 modèles, PostgreSQL, nommage snake_case en base et camelCase côté Prisma (via @map).

4.1 Énumérations#

Enum Valeurs
Role admin, technician, user
TicketStatus new, in_progress, waiting, resolved, closed
TicketPriority low, medium, high
CommentType comment (message humain), event (entrée de journal)
AssetType pc, printer, server, software
AssetStatus in_service, in_repair, retired
FieldType text, textarea, select, date, checkbox

L'ordre de déclaration des enums compte : le tri par statut ou par priorité en base suit l'ordre de l'enum PostgreSQL (newclosed, lowhigh), pas l'ordre alphabétique. C'est ce qui rend le tri « priorité haute d'abord » correct sans colonne numérique dédiée.

4.2 Entités#

Useremail unique, passwordHash nullable (les comptes SSO n'en ont pas), provider (local | entra), role, teamId optionnel.

Team, Category — deux référentiels plats (id, nom). La catégorie porte les tickets, les formulaires et les articles de la base de connaissances.

Ticket — cœur du modèle. title, description, status, priority, plus six liens : category (requis), author (requis), assignee, team, asset, form. satisfaction (1/0/null) et satisfactionAt portent l'enquête un clic. updatedAt est géré par Prisma (@updatedAt) et sert de base à la clôture automatique.

TicketComment — messages et journal d'événements dans la même table, discriminés par type. Les événements sont générés par les routes et par le moteur de workflows, et sont écrits dans la même transaction que la modification qu'ils décrivent (comments: { create: … } imbriqué dans le update). Suppression en cascade avec le ticket.

Attachment — métadonnées seules ; le fichier vit sur le disque (storedPath). Cascade avec le ticket — attention, le fichier n'est pas supprimé du disque (voir §14).

Asset — inventaire : nom, type, emplacement, date d'achat, état, utilisateur assigné. Un actif référencé par des tickets ne peut pas être supprimé (409) : il faut le passer en retired.

Form / FormField — catalogue de demandes. Un formulaire cible une catégorie et une priorité ; ses champs sont ordonnés par position, et les choix d'un champ select sont stockés en JSON sérialisé dans options (colonne String, pas Json). Les champs sont supprimés en cascade et remplacés en bloc à chaque édition, jamais diffés.

Setting — table clé/valeur JSON. Trois familles de clés : app.config (paramètres globaux), dashboard.layout.<role> (layout imposé par rôle), dashboard.layout.user.<id> (layout personnel d'un compte).

KbArticle — titre, corps, published, catégorie optionnelle, auteur.

Workflow / WorkflowStep / WorkflowRun — voir §7.

4.3 Diagramme relationnel#

Team ──< User ──< Ticket >── Category
                    │  │        │
                    │  │        ├──< Form ──< FormField
                    │  │        └──< KbArticle
                    │  ├──< TicketComment
                    │  ├──< Attachment
                    │  └──< WorkflowRun >── Workflow ──< WorkflowStep
                    └── Asset

4.4 Migrations#

11 migrations, appliquées dans l'ordre :

Migration Apport
init socle : users, teams, categories, tickets, comments, attachments, assets
add_forms catalogue de demandes
ticket_satisfaction enquête un clic
kb_articles base de connaissances
settings table clé/valeur
workflows workflows + étapes
drop_workflow_rules suppression des anciennes « règles »
workflow_runs position d'un ticket dans un workflow
workflow_canvas_layout coordonnées du canvas
workflow_branches blocs condition, graphe edges
sso_auth password_hash nullable + provider

5. Authentification et autorisation#

5.1 Jeton#

POST /api/auth/login vérifie le couple email/mot de passe avec bcrypt puis émet un JWT HS256 signé avec JWT_SECRET :

{ "sub": 12, "role": "technician", "teamId": 3, "iat": …, "exp": … }

Durée de vie : 7 jours, sans rafraîchissement ni révocation. Le client le stocke dans localStorage sous la clé parqueo_token et l'envoie en Authorization: Bearer ….

authRequired (middleware/auth.js) vérifie le jeton et pose req.user. requireRole(...roles) (middleware/roles.js) filtre ensuite par rôle.

Conséquence importante : les changements de rôle, d'équipe ou la suppression d'un compte ne prennent effet qu'à l'expiration du jeton, puisque req.user provient du jeton et non d'une lecture en base.

5.2 Visibilité des tickets#

lib/visibility.js produit un where Prisma appliqué à toutes les lectures de tickets :

Rôle Tickets visibles
admin tous
technician ceux qui lui sont assignés, ou non assignés, ou portés par son équipe
user ceux dont il est l'auteur

L'inventaire a sa propre règle, plus simple (routes/assets.js) : un user ne voit que les actifs qui lui sont assignés, et l'accès complet à l'inventaire peut lui être coupé par le paramètre assetsVisibleToUsers.

5.3 SSO Microsoft Entra ID#

Implémentation OIDC authorization code flow, client confidentiel, sans dépendance externe (services/oidc.js). Activé dès que SSO_TENANT_ID, SSO_CLIENT_ID et SSO_CLIENT_SECRET sont renseignés.

Navigateur          API Parqueo                      Entra ID
    │  GET /api/auth/sso/login                           │
    │─────────────────►│                                 │
    │                  │ state = JWT{nonce, purpose}     │
    │                  │ (10 min, signé JWT_SECRET)      │
    │◄─── 302 ─────────│                                 │
    │──────────────── authorize?…&state&nonce ──────────►│
    │◄──────────────── 302 code ─────────────────────────│
    │  GET /api/auth/sso/callback?code&state             │
    │─────────────────►│                                 │
    │                  │ vérifie state → nonce           │
    │                  │──── POST /token (secret) ──────►│
    │                  │◄─── id_token ───────────────────│
    │                  │ vérifie RS256 via JWKS,         │
    │                  │ audience, issuer, nonce         │
    │                  │ garde-fou domaines              │
    │                  │ provisioning JIT (rôle user)    │
    │◄── 302 ?sso_token=… ────────────────────────────────

Points d'implémentation :

Un compte SSO qui tente le formulaire mot de passe reçoit un message dédié (« Ce compte utilise la connexion Microsoft. »), puisque passwordHash est nul.

5.4 Limitation de débit#

Seul /api/auth/login est protégé : 20 tentatives par fenêtre de 15 minutes, avec skipSuccessfulRequests: true — les connexions réussies ne consomment pas le quota, pour qu'une équipe entière derrière une même IP (NAT) ne se bloque pas elle-même.


6. API HTTP#

Préfixe commun /api. Toutes les routes exigent un jeton sauf GET /api/health, GET /api/auth/config, POST /api/auth/login, les deux routes SSO et GET /api/tickets/satisfaction.

Erreurs : { "error": "message en français" } avec un statut cohérent (400 validation, 401 auth, 403 rôle, 404 introuvable ou invisible, 409 conflit). Le 404 est volontairement utilisé pour l'invisible : un technicien ne peut pas distinguer un ticket inexistant d'un ticket qu'il n'a pas le droit de voir.

6.1 Santé et authentification#

Méthode Chemin Accès Rôle
GET /api/health public
GET /api/auth/config public expose { sso: bool } pour afficher ou non le bouton SSO
POST /api/auth/login public { email, password }{ token, user }
GET /api/auth/sso/login public redirige vers Entra
GET /api/auth/sso/callback public retour Entra → redirection avec sso_token
GET /api/auth/me authentifié profil courant

6.2 Tickets#

Méthode Chemin Rôle Notes
GET /api/tickets/satisfaction public (lien signé) ?token=&value=up|down, répond en HTML
GET /api/tickets tous filtres + pagination, voir ci-dessous
GET /api/tickets/:id tous détail + commentaires + pièces jointes + état des workflows
POST /api/tickets tous crée, déclenche workflows et notification
PATCH /api/tickets/:id admin, technicien statut, priorité, assignation, équipe, actif, titre, description
POST /api/tickets/:id/comments tous (si visible) ajoute un message
POST /api/tickets/:id/attachments tous (si visible) multipart/form-data, champ file
GET /api/tickets/:id/attachments/:attachmentId tous (si visible) téléchargement

Filtres de liste : status, priority, assigneeId (un id ou none), categoryId, teamId, q (titre ou description, insensible à la casse), sort.

Tri : présélections oldest, priority, ou colonne-direction parmi id, status, priority, category, assignee, updated × asc|desc. Défaut : updatedAt desc.

Pagination : sans page, la réponse est un tableau brut (utilisé par le tableau de bord). Avec page, elle devient :

{ "items": [...], "total": 128, "counts": { "new": 12, "in_progress": 30, … } }

pageSize accepte un entier (borné à 500) ou all. counts est calculé hors filtre de statut, pour que les pastilles de la liste affichent toujours tous les statuts.

Le détail GET /:id enrichit la réponse d'un tableau workflows : [{ name, waiting }], où waiting est le libellé lisible du bloc d'attente sur lequel le ticket est parqué (« en attente de prise en charge », « en attente du statut « Résolu » »).

6.3 Inventaire#

Méthode Chemin Rôle
GET /api/assets tous (filtré) — ?type=&status=&q=
GET /api/assets/:id tous (filtré) — inclut l'historique des tickets
POST /api/assets admin, technicien
PATCH /api/assets/:id admin, technicien
DELETE /api/assets/:id admin — 409 si des tickets le référencent

6.4 Utilisateurs#

Méthode Chemin Rôle
GET /api/users/assignable admin, technicien — techniciens + admins
GET /api/users admin
POST /api/users admin — mot de passe ≥ 8 caractères
POST /api/users/import admin — import en masse, voir ci-dessous
PATCH /api/users/:id admin
DELETE /api/users/:id admin

Import en masse : { users: [{ name, email, role?, team? \| teamId?, password? }] }, 1000 lignes maximum. Le traitement est ligne à ligne et non transactionnel : un compte rendu par ligne (created, skipped, error) est renvoyé, et un mot de passe aléatoire (~12 caractères, randomBytes(9).toString('base64url')) est généré et retourné en clair dans la réponse quand la ligne n'en fournit pas. C'est le seul moment où ce mot de passe est lisible.

Garde-fous : on ne peut ni retirer son propre rôle admin, ni supprimer son propre compte ; un compte porteur de tickets ou de commentaires ne peut pas être supprimé (409).

6.5 Workflows, formulaires, base de connaissances, référentiels#

Méthode Chemin Rôle
GET/POST/PUT/DELETE /api/workflows[/:id] admin uniquement
GET /api/forms tous — actifs seulement, ?all=1 pour l'admin
GET /api/forms/:id tous
POST /api/forms/:id/submit tous — crée le ticket
POST/PATCH/DELETE /api/forms[/:id] admin
GET /api/kb, /api/kb/:id tous — les user ne voient que le publié
POST/PATCH/DELETE /api/kb[/:id] admin, et techniciens si kbTechniciansWrite
GET /api/categories, /api/teams tous
POST /api/categories, /api/teams admin

PUT /api/workflows/:id remplace le workflow en entier (métadonnées, étapes, fils, positions du canvas) dans une transaction Prisma : update + deleteMany(steps) + createMany(steps). Il n'y a pas d'édition incrémentale d'une étape.

La recherche de la base de connaissances découpe q en mots d'au moins 3 caractères (8 maximum) et cherche chacun dans le titre ou le corps. C'est la même route qui alimente les suggestions à la création de ticket (?take=3).

6.6 Paramètres#

Méthode Chemin Rôle
GET /api/settings/app tous — le client en a besoin pour adapter l'UI
PATCH /api/settings/app admin
GET /api/settings/dashboard tous — ?role= réservé à l'admin
PUT/DELETE /api/settings/dashboard/personal tous, si la permission est accordée
PUT/DELETE /api/settings/dashboard/:role admin

7. Moteur de workflows#

services/workflowEngine.js + lib/workflowUtils.js. C'est la partie la plus dense du logiciel ; le reste est du CRUD.

7.1 Modèle#

Un workflow est un graphe orienté :

7.2 Types de blocs#

Catégorie Type Configuration Effet
Action assign_team teamId affecte l'équipe
Action assign_user userId assigne (refusé si le compte est un user)
Action set_priority priority change la priorité
Action set_status status change le statut
Action add_note body ajoute une entrée de journal (gabarit)
Action send_email to (author|assignee|custom), email, subject, body envoie un email (gabarit)
Action webhook url, secret POST JSON
Branche condition field, value deux sorties oui/non
Attente wait_assigned parque jusqu'à la prise en charge
Attente wait_status status parque jusqu'à un statut donné

Champs testables par condition : priority, status, category, form, assigned (le ticket a-t-il un assigné).

Les blocs d'attente ne sont proposés dans l'éditeur que sur le déclencheur ticket_created (catalogFor côté client) : un workflow réactif ne parque pas.

7.3 Exécution#

onTicketCreated(ticket)                    onTicketUpdated(ticket, ctx)
   │                                          │
   │ workflows ticket_created actifs          │ si statut changé :
   │ dont les conditions matchent             │   workflows status_changed
   ▼                                          │   → traverse(sans run)
startRun ── crée un WorkflowRun               │
   │        (unique par ticket × workflow)    ▼
   ▼                                       resumeRuns ── pour chaque run actif,
traverse(edges.trigger)                       reprend à run.nodeKey

traverse() parcourt le graphe depuis une clé de départ :

Un Set de clés visitées empêche toute boucle infinie dans un graphe cyclique : chaque bloc est exécuté au plus une fois par parcours.

Reprise. PATCH /api/tickets/:id rappelle le moteur dès que le statut ou l'assignation change — ce sont précisément les deux événements qui peuvent débloquer wait_assigned et wait_status. Un ticket parqué repart alors du bloc où il attendait, la condition étant revérifiée au passage.

Anti-boucle. Les modifications faites par une étape passent par prisma.ticket.update direct et ne repassent jamais par onTicketUpdated : un set_status dans un workflow ne peut pas déclencher un workflow status_changed. La propriété est structurelle, pas conditionnelle.

Idempotence d'entrée. WorkflowRun porte une contrainte @@unique([ticketId, workflowId]) et startRun sort si un run existe déjà : un ticket n'entre qu'une fois dans un workflow donné, même si l'événement se répète.

7.4 Gabarits#

renderTemplate remplace {{clé}} dans les notes et les emails de workflow. Variables disponibles : ticket.id, ticket.title, ticket.description, ticket.status, ticket.priority, author.name, author.email, assignee.name, assignee.email, category.name, team.name. Une variable inconnue ou nulle est remplacée par une chaîne vide.

7.5 Webhook#

POST JSON, corps { event, workflow, ticket }event est le déclencheur du workflow. En-têtes : Content-Type: application/json et, si un secret est configuré, X-Parqueo-Token: <secret>. Timeout 5 secondes via AbortController. Seules les URL http:// ou https:// sont acceptées ; la réponse n'est ni lue ni vérifiée, et un échec ne bloque pas le parcours.

7.6 Validation à l'enregistrement#

routes/workflows.js nettoie tout ce qui vient du client :

Un canvas incohérent ne peut donc pas produire un graphe d'exécution invalide.


8. Services de fond#

8.1 Notifications sortantes (mailer.js)#

Quatre notifications, chacune débrayable dans les paramètres :

Fonction Déclencheur Destinataires
notifyTicketCreated création demandeur + assigné
notifyStatusChanged changement de statut demandeur + assigné
notifyAssigned assignation le technicien qui reçoit (pas soi-même)
notifyCommentAdded nouveau message l'autre partie, jamais l'auteur du message

Sans SMTP_HOST, rien n'est envoyé : le message est écrit en console ([mail non envoyé — SMTP non configuré]). C'est le mode de développement par défaut.

Un échec d'envoi est attrapé et journalisé : il ne fait jamais échouer la requête HTTP en cours. Les appels sont d'ailleurs lancés sans await depuis les routes.

Enquête de satisfaction : quand un ticket passe à resolved et que satisfactionSurvey est actif, l'email contient deux liens signés (JWT purpose: 'satisfaction', valable 30 jours) vers /api/tickets/satisfaction. La route est publique — le jeton est l'authentification — et n'accepte qu'un seul vote par ticket.

8.2 Collecteur email (mailbox.js)#

Activé seulement si IMAP_HOST est renseigné. Boucle de relève :

  1. connexion ImapFlow, verrou sur INBOX, recherche des messages non lus ;
  2. chaque message est marqué lu avant traitement — un email qui fait planter le parseur ne sera pas retraité en boucle à chaque cycle ;
  3. parsing mailparser, puis handleIncomingEmail.

Règles de traitement :

Intervalle : IMAP_POLL_SECONDS (défaut 60), plancher à 15 secondes. Un verrou running empêche deux relèves simultanées, et l'intervalle est unref() pour ne pas retenir le processus.

8.3 Clôture automatique (autoclose.js)#

Au démarrage puis toutes les heures : les tickets resolved dont updatedAt est antérieur à maintenant − autoCloseDays passent en closed, avec un événement de journal explicite. autoCloseDays: 0 désactive le mécanisme. L'auteur de l'événement est l'assigné, à défaut le demandeur.

Le paramètre vient de la base (app.config) ; la variable d'environnement AUTO_CLOSE_DAYS n'en est que la valeur initiale par défaut.


9. Pièces jointes#


10. Paramètres applicatifs#

Une seule ligne settings (clé app.config), fusionnée avec APP_DEFAULTS, avec un cache mémoire de 10 secondes (les routes et les emails lisent souvent).

Clé Défaut Effet
dashboardPersonalTechnician false les techniciens peuvent personnaliser leur tableau de bord
dashboardPersonalUser false idem pour les utilisateurs
ticketDefaultPriority medium priorité appliquée à défaut
userCanSetPriority true un utilisateur peut choisir la priorité
autoCloseDays AUTO_CLOSE_DAYS ou 7 délai de clôture automatique (0 = désactivé)
satisfactionSurvey true enquête un clic à la résolution
notifyOnCreate true email à la création
notifyOnStatus true email au changement de statut
notifyOnAssign true email à l'assignation
notifyOnComment true email au nouveau message
kbSuggest true suggestions d'articles à la création de ticket
kbTechniciansWrite true les techniciens rédigent dans la base de connaissances
assetsVisibleToUsers true l'inventaire est visible des utilisateurs

Validation à l'écriture : ticketDefaultPriority doit être une priorité connue, autoCloseDays un entier de 0 à 365, toute autre clé est booléenne. Une clé inconnue est refusée (400) — le jeu de clés est fermé.


11. Front-end#

11.1 Routage#

SPA React Router. Protected exige une session, AdminOnly filtre /admin et /parametres. Toute route inconnue redirige vers /.

Chemin Page
/login connexion (mot de passe + bouton SSO conditionnel)
/ tableau de bord modulaire
/tickets liste filtrable et paginée
/tickets/nouveau catalogue de demandes
/tickets/nouveau/libre demande libre
/demandes/:id remplissage d'un formulaire
/tickets/:id détail d'un ticket
/aide, /aide/:id, /aide/nouveau base de connaissances
/inventaire, /inventaire/:id parc
/admin utilisateurs, équipes, catégories, formulaires, workflows
/parametres paramètres globaux

11.2 Accès à l'API#

api/client.js : wrapper fetch minimal qui pose le Bearer, sérialise le JSON, gère le 204 et transforme toute erreur en ApiError { status, message }. Il n'y a pas d'intercepteur de 401 : un jeton expiré produit une erreur par appel, et AuthContext ne purge la session qu'au chargement initial (/auth/me en échec).

En développement, Vite proxifie /api vers http://localhost:4000 (vite.config.js). En production, c'est le rôle du reverse proxy.

11.3 Tableau de bord modulaire#

lib/dashboard.js définit :

Grille de 4 colonnes ; chaque widget déclare les tailles qu'il accepte (1, 2 ou 4). Les layouts par rôle sont imposés par l'administration ; un layout personnel ne s'applique que si la permission correspondante est accordée, et il est persisté en base (donc synchronisé entre navigateurs), pas en localStorage.

11.4 Éditeur de workflows#

components/WorkflowCanvas.jsx (~770 lignes) : canvas SVG avec blocs déplaçables, fils tracés entre les points de connexion, sorties oui/non pour les conditions, et panneau de configuration par bloc. lib/workflowBlocks.js porte le catalogue partagé (libellés, icônes, couleurs, configuration par défaut) — il doit rester aligné avec STEP_TYPES côté serveur.

Éditeur de workflow : six blocs reliés par des fils, avec les deux sorties oui/non d'un bloc condition.
Le canvas rend directement le graphe steps + edges décrit au §7.1 : chaque bloc porte sa key, chaque fil est une entrée de edges, et les coordonnées sont persistées dans x/y.

11.5 Tableau générique#

components/DataTable.jsx : filtre par colonne (liste des valeurs distinctes ou recherche texte), tri cyclique au clic (desc → asc → aucun) et pagination 10/25/50/100/500/Tout. Le tri et le filtrage sont côté client — c'est adapté aux référentiels (utilisateurs, actifs), pas à un volume de tickets important, raison pour laquelle la liste des tickets a sa propre implémentation serveur.


12. Tests#

npm test dans server/node --test 'src/**/*.test.js' (exécuteur natif Node, aucun framework).

Fichier Couverture
lib/inbound.test.js extraction de l'id de ticket, suppression des citations, normalisation d'adresse
lib/visibility.test.js règle de visibilité pour les trois rôles
lib/workflowUtils.test.js conditions, blocs d'attente, branches, gabarits

26 tests, tous au vert à la date de rédaction. La couverture est volontairement concentrée sur la logique pure. Il n'y a aucun test d'intégration HTTP ni de test de bout en bout : les routeurs, le moteur en situation réelle et le front ne sont pas couverts.

Attention : node --test src/ (sans motif) importerait src/index.js et démarrerait un vrai serveur — utilisez le script npm test.

Le front a npm run lint (oxlint) mais pas de tests.


13. Sécurité#

Ce qui est en place :

Points à connaître avant une mise en production :

  1. Jeton en localStorage. Une faille XSS dans le front donnerait accès au jeton. Le choix évite CSRF et simplifie le déploiement ; il faut en contrepartie tenir le front propre.
  2. Jeton non révocable, 7 jours. Changer un rôle, retirer une équipe ou supprimer un compte ne coupe pas les sessions en cours. Pour une révocation immédiate, il faut faire tourner JWT_SECRET (ce qui déconnecte tout le monde).
  3. sso_token transite dans l'URL. Il est retiré de la barre d'adresse immédiatement, mais il peut apparaître dans l'historique du navigateur et dans les journaux d'accès du reverse proxy. Filtrez les query strings dans vos logs, ou passez à un cookie HttpOnly si votre politique l'exige.
  4. Collecteur email non authentifié cryptographiquement. L'expéditeur est vérifié par simple correspondance d'adresse : ni SPF, ni DKIM, ni DMARC ne sont contrôlés. Une adresse usurpée d'un utilisateur connu peut créer un ticket ou commenter en son nom. À faire filtrer en amont par le serveur de messagerie.
  5. Liste blanche d'extensions, pas de reniflage de contenu. Un exécutable renommé .pdf est accepté. Les fichiers ne sont jamais exécutés ni servis en statique, mais ils sont téléchargeables par les personnes qui voient le ticket.
  6. Limitation de débit sur le seul login. Le reste de l'API n'a pas de quota ; ajoutez-en un au niveau du reverse proxy si l'API est exposée publiquement.
  7. Webhook sortant sans filtrage d'adresse. Un administrateur peut faire pointer un bloc webhook vers une IP interne (SSRF). Le rôle admin est de toute façon un rôle de confiance, mais l'API cloud interne d'un hébergeur reste atteignable.
  8. Mot de passe de seed par défaut (admin@parqueo.local / admin1234) : à changer impérativement au premier démarrage.

14. Limites connues#