Chapitre 01Contexte et besoin
Le recrutement dans la messagerie et la logistique se distingue de celui des fonctions tertiaires sur quatre points, qui commandent toute la conception.
- Le volume. Une plateforme de tri recrute par dizaines, pas par unités. Un poste d'agent de quai attire couramment plusieurs dizaines de candidatures, souvent peu différenciées.
- Les prérequis réglementaires. Permis C ou CE, FIMO/FCO, carte conducteur, habilitation ADR, CACES 1/3/5, habilitation électrique : ce sont des conditions d'accès à l'emploi, pas des « plus ». Un dossier sans le titre requis ne peut pas avancer, et le vérifier tardivement coûte plusieurs jours à tout le monde.
- Les contraintes d'organisation. Horaires 2×8, 3×8, nuit, week-end, saisonnalité du pic de fin d'année : la disponibilité est un critère de sélection à part entière.
- La dispersion géographique. Plusieurs entités, une douzaine de sites, des besoins qui se déplacent d'une agence à l'autre au fil de l'activité.
Un suivi sur tableur partagé ne tient sur aucun de ces points : pas de traçabilité des décisions, pas de vérification systématique des habilitations, pas de visibilité consolidée, et surtout aucune gestion de la durée de conservation des CV — un risque RGPD direct.
Chapitre 02État de l'art
Une revue des plateformes du marché et des exigences de conformité a précédé la conception. Voici les constantes retenues, et ce qui en a été fait ici.
| Constante du marché | Traduction dans le projet |
|---|---|
| Analyse automatique des CV en données structurées | Extraction du texte des PDF, DOCX et RTF, détection des coordonnées et de l'ancienneté |
| Score de correspondance candidat / poste | Moteur pondéré par critère, avec critères éliminatoires, restitué en clair |
| Pipeline à étapes configurables | Neuf étapes en table de référence, vue kanban avec glisser-déposer |
| Workflow d'approbation des demandes | Six statuts, transitions contrôlées, approbation réservée au manager |
| Piste d'audit des décisions | Journal en ajout seul et chronologie métier par candidature |
| Conformité RGPD | Conservation 24 mois, anonymisation automatique, export ZIP normalisé |
| Analytique temps réel | Tableau de bord, rapports, exports CSV |
| Expérience candidat mobile | Portail responsive, dépôt en trois minutes, suivi par référence |
Sources consultées
- Modern ATS: What to Look For in 2026 — Lever
- What Is an Applicant Tracking System? — Jobscan
- How Applicant Tracking Systems Work (8-Step Data Flow) — Reqcore
- Applicant Tracking Systems: A Complete Guide — Bullhorn
- Building a Custom ATS Workflow — Tracker
- ATS Compliance: GDPR, EEOC & DEI Requirements — Kula
- GDPR compliance guide for recruitment — Workable
- GDPR in recruitment: what employers need to know in 2026 — Recruitee
Chapitre 03Périmètre livré
Portail candidat
| Page | Fonction |
|---|---|
| / | Accueil : recherche, familles de métiers, offres du moment, chiffres en direct |
| /offres | Liste et filtres : mot-clé, métier, ville, contrat, horaires |
| /offres/{ref} | Annonce complète : missions, profil, avantages, conditions |
| /offres/{ref}/postuler | Candidature : coordonnées, CV, lettre, questions de présélection, consentement |
| /suivi | Suivi par référence et e-mail, avec la progression étape par étape |
| /candidature-spontanee | Dépôt hors offre, orienté vers le vivier |
| /confidentialite | Politique de traitement des données candidat |
Back-office RH
- Tableau de bord — offres publiées, candidatures à 30 jours, dossiers en cours, entretiens de la semaine, embauches à 90 jours, délai moyen de recrutement, taux de conversion, entonnoir, origine des candidatures, offres à surveiller.
- Réquisitions — création, critères pondérés, questions de présélection, workflow brouillon → à approuver → approuvée → publiée → pourvue / annulée.
- Pipeline — kanban à glisser-déposer, filtres par offre, score et profils éliminés.
- Fiche candidature — analyse de correspondance détaillée, CV et texte extrait, réponses de présélection, chronologie, notes, évaluations multi-évaluateurs, entretiens, envoi d'e-mails depuis modèle.
- Base de candidats — recherche plein texte dans le contenu des CV, vivier.
- Entretiens — planification, compte rendu, agenda.
- Rapports — volumes, entonnoir, performance par offre, efficacité des sources, motifs de refus, exports CSV.
- Administration — utilisateurs et rôles, référentiels, modèles d'e-mails, file d'envoi, console RGPD, journal d'audit.
Rôles
| Rôle | Portée |
|---|---|
| Administrateur RH | Tout, y compris administration et RGPD |
| Chargé de recrutement | Réquisitions, pipeline, candidats, e-mails |
| Manager opérationnel | Ses réquisitions, évaluations, approbation |
| Lecture seule | Consultation et rapports uniquement |
Chapitre 04Architecture
Le serveur cible impose deux contraintes : pas de Node.js et pas de base PostgreSQL active. Elles ont orienté toute la pile.
| Couche | Choix | Pourquoi |
|---|---|---|
| Serveur | Starlette + Uvicorn | 100 % Python pur. Python 3.14 étant récent, toute dépendance compilée (pydantic-core en Rust, lxml en C) exposait à une roue manquante. L'application étant en formulaires rendus côté serveur, l'apport de FastAPI aurait été marginal. |
| Rendu | Jinja2, CSS et JavaScript écrits à la main | Aucun outil de construction, donc aucun besoin de Node. L'application reste utilisable sans JavaScript. |
| Base | SQLite en mode WAL | Volumétrie cible très en deçà des limites, sauvegarde par instantané, exploitation nulle. Tous les accès passent par app/db.py : une bascule PostgreSQL ne toucherait que ce fichier. |
| Recherche CV | SQLite FTS5 | Index plein texte natif, sans moteur externe. |
| Lecture des CV | pypdf + zipfile/xml.etree | pypdf est pur Python ; lire word/document.xml avec la bibliothèque standard évite python-docx et sa dépendance lxml compilée. |
| Génération de PDF | Écriture directe du format | Les CV de démonstration sont de vrais PDF. Quarante lignes de code ont remplacé une dépendance lourde. |
| Mots de passe | hashlib.scrypt | Robuste, dans la bibliothèque standard, aucun risque de roue absente. |
| Sessions | Cookie signé + table de sessions | Signature côté client, révocation côté serveur. |
Chaîne de traitement d'une candidature
Candidat Application Base
│ │ │
├── formulaire ──────────────▶│ │
│ ├── contrôle CSRF │
│ ├── validation du fichier │
│ │ (extension + signature │
│ │ binaire + taille) │
│ ├── nom régénéré en UUID ───────▶│ /uploads (hors webroot)
│ ├── extraction du texte │
│ │ (hors boucle d'événements) │
│ ├── moteur de correspondance ───▶│ score + détail
│ ├── indexation FTS5 ────────────▶│ documents_fts
│ ├── chronologie + audit ────────▶│ application_events
│◀── référence DHL-AAAA-NNNNNN┤ │
│ └── accusé de réception ────────▶│ email_outbox
Modèle de données — 26 tables
| Domaine | Tables |
|---|---|
| Accès | users, sessions |
| Référentiels | business_units, sites, job_families, stages |
| Besoins | requisitions, requisition_criteria, screening_questions |
| Candidatures | candidates, applications, application_answers, documents, documents_fts |
| Suivi | application_events, evaluations, interviews, notes |
| Communication | email_templates, email_outbox |
| Traçabilité | audit_log, settings |
L'index plein texte est tenu à jour par trois déclencheurs SQL (AFTER INSERT, AFTER UPDATE, AFTER DELETE) : aucune synchronisation applicative à maintenir.
Chapitre 05Moteur de correspondance
Le score est explicable par construction. Il ne sert pas à décider à la place du recruteur mais à trier une pile : un score bas n'écarte personne, il descend le dossier dans la liste.
Calcul
- Le texte du CV est normalisé : minuscules, accents supprimés.
- Chaque critère de la réquisition porte des synonymes, un poids de 1 à 5, et éventuellement un drapeau éliminatoire. Score de base = Σ(poids obtenus) / Σ(poids totaux) × 100.
- L'ancienneté détectée dans le CV donne un bonus de 0 à 10 points, proportionné au niveau demandé sur l'offre.
- Une réponse non conforme à une question éliminatoire, ou un critère obligatoire absent, place le dossier en éliminé : son score est plafonné à 35 et le motif est affiché explicitement.
Détection
La recherche exige une frontière de mot des deux côtés, avec tolérance au pluriel : « logistique » trouve « logistiques », mais « CAP » n'est pas déclenché par « capacité ». Ce point précis a été révélé par les tests unitaires et corrigé.
Restitution
Le recruteur ne voit jamais un nombre seul, mais chaque critère avec le terme effectivement trouvé dans le CV :
- Score sur critères
- 100/100
- Bonus expérience
- +10
- Score retenu
- 100/100
Chapitre 06Sécurité
| Mesure | Mise en œuvre |
|---|---|
| Transport | HTTPS avec certificat Let's Encrypt, redirection permanente depuis HTTP, HSTS un an |
| Mots de passe | scrypt (n=2¹⁴, r=8, p=1), sel aléatoire de 16 octets, comparaison à temps constant |
| Sessions | Cookie signé HttpOnly + Secure + SameSite=Lax, expiration 8 h, révocation en base |
| CSRF | Jeton signé et ancré à la session sur tous les POST, y compris les formulaires publics |
| Injection SQL | Requêtes exclusivement paramétrées |
| XSS | Échappement automatique Jinja2 ; les deux filtres produisant du HTML échappent avant d'insérer |
| Dépôts de fichiers | Extension et signature binaire contrôlées, 5 Mo maximum, nom régénéré en UUID, stockage hors racine web |
| Accès aux CV | Uniquement via une route authentifiée, avec vérification anti-traversée de chemin |
| Cloisonnement | Contrôle de rôle par décorateur sur chaque route du back-office |
| Suppression de compte | Réservée à l'administrateur, refusée sur soi-même et sur le dernier administrateur actif ; le travail produit survit, détaché de son auteur, et le journal d'audit conserve une trace nominative |
| En-têtes | CSP stricte default-src 'self', nosniff, Referrer-Policy, X-Frame-Options, Permissions-Policy |
| Débit | Limitation nginx spécifique sur la connexion, le dépôt de candidature et le suivi ; fail2ban actif |
| Énumération | La page de suivi renvoie une réponse identique que la référence existe ou non |
| Isolation du service | systemd : NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, PrivateTmp |
| Traçabilité | Journal d'audit en ajout seul : acteur, action, objet, avant/après, IP, horodatage |
Deux arbitrages documentés
La snippet nginx security-headers.conf partagée par les autres sites de la machine autorise plusieurs CDN externes. L'inclure aurait produit un second en-tête CSP et affaibli la lecture de la configuration. Le vhost ne l'inclut donc pas : l'application émet sa propre CSP stricte, et nginx ne complète que le HSTS.
De même, les fichiers statiques sont servis par l'application et non par nginx : donner à www-data un droit de traversée sur /home/ubuntu pour trois fichiers aurait été un mauvais échange.
Chapitre 07Conformité RGPD
| Exigence | Mise en œuvre |
|---|---|
| Consentement | Case explicite non pré-cochée, horodatée, avec version de la politique conservée |
| Information | Page dédiée : qui traite, quoi, pourquoi, combien de temps, qui accède, quels droits |
| Durée de conservation | 24 mois (recommandation CNIL), échéance calculée à la création du dossier |
| Effacement | Anonymisation plutôt que suppression : identité neutralisée, CV effacés du disque, notes purgées — les statistiques de recrutement restent exploitables |
| Automatisation | Timer systemd quotidien à 03h30 ; l'opération est tracée dans le journal d'audit |
| Portabilité | Export ZIP : un JSON complet (identité, candidatures, réponses, entretiens, évaluations, communications) et l'ensemble des documents |
| Pilotage | Console RGPD : dossiers arrivant à échéance sous 60 jours, historique des purges, registre du traitement |
| Minimisation | Aucune donnée sensible collectée ; pas de sous-traitant, pas de transfert hors UE |
Chapitre 08Déploiement
Cible : un serveur Ubuntu (2 vCPU, 7 Go), nginx et certbot déjà en place, en cohabitation avec d'autres applications.
Internet ──▶ nginx (443) ──▶ uvicorn 127.0.0.1:8090 ──▶ SQLite (WAL)
│ TLS Let's Encrypt │
│ limitation de débit └──▶ /uploads (hors webroot)
└ redirection 80 → 443
| Composant | Détail |
|---|---|
| Service | dhl-ats.service — uvicorn sur 127.0.0.1:8090, redémarrage automatique, durcissement systemd |
| Frontal | careers.dhl.d3v-test.me — TLS, HSTS, limitation de débit ciblée, corps limité à 6 Mo |
| Certificat | Let's Encrypt, émis le 21/07/2026, valide jusqu'au 19/10/2026, renouvellement automatique |
| Purge RGPD | dhl-ats-purge.timer — quotidienne à 03h30 |
| Sauvegarde | dhl-ats-backup.timer — quotidienne à 02h00, instantané SQLite cohérent et archive des CV, rétention 14 jours |
| Supervision | /healthz (application), /nginx-health (frontal), journaux séparés |
Le port 8090 a été retenu après vérification : 8000 et 27017 sont déjà occupés par des conteneurs Docker sur cette machine.
Chapitre 09Recette
Tests automatisés — 39 tests, tous passants
| Domaine | Couverture |
|---|---|
| Moteur de scoring | Pondération, critères éliminatoires, synonymes, insensibilité aux accents, frontière de mot, questions éliminatoires, bonus d'expérience, bornes du score, sérialisation, données corrompues |
| Extraction et sécurité | Extraction PDF et DOCX, détection des coordonnées et de l'ancienneté, robustesse aux fichiers illisibles, hachage des mots de passe |
| Bout en bout | Parcours candidat complet, anti-doublon, consentement obligatoire, rejet CSRF, rejet d'un faux PDF, suivi, cloisonnement des rôles, changement d'étape, déclenchement d'e-mail, exports CSV, workflow de réquisition et transition illégale, export et anonymisation RGPD, protection des documents |
$ .venv/bin/python -m unittest discover -s tests
Ran 39 tests in 1.287s
OK
Trois défauts réels détectés et corrigés
- normalize() ne supprimait pas les espaces de bord, ce qui rendait inopérant tout synonyme placé après une virgule dans la liste des mots-clés — la majorité d'entre eux.
- contient() n'exigeait une frontière de mot qu'à gauche : « CAP » était détecté dans « capacité ».
- Le jeton CSRF était signé avec un identifiant anonyme que la réponse n'avait pas encore posé, si bien que le tout premier formulaire servi à un visiteur était systématiquement rejeté. Corrigé en fixant l'identifiant avant la signature.
Recette sur le site en production — 18 contrôles sur 18
— Portail public — OK page d'accueil rendue OK liste des offres (13 offres) OK filtre par mot-clé OK détail d'une offre OK candidature déposée avec CV PDF (DHL-2026-000079) OK suivi de candidature par référence — Back-office RH — OK connexion recruteur OK la candidature apparaît dans le pipeline OK pages tableau de bord, réquisitions, candidats, entretiens, rapports OK recherche plein texte dans les CV OK export CSV (79 lignes) — Cloisonnement des rôles — OK un compte lecture seule est refusé sur /rh/admin OK un visiteur anonyme est redirigé vers la connexion OK les CV ne sont pas téléchargeables sans authentification
Contrôles complémentaires : déplacement kanban accepté (200) et rejeté avec un jeton CSRF invalide (400) ; certificat TLS au bon nom ; en-tête CSP unique ; redirection HTTP 301 ; purge RGPD et sauvegarde exécutées avec succès.
Chapitre 10Volumétrie
Jeu de démonstration chargé : 15 réquisitions dont 13 publiées, 79 candidats, 79 candidatures réparties dans le pipeline, 79 CV au format PDF réellement analysés, 29 entretiens, 259 événements de chronologie.
Chapitre 11Limites connues
- Les e-mails ne partent pas. Ils sont déposés dans une file consultable, telle qu'ils seraient expédiés. Brancher un SMTP consiste à implémenter une seule fonction.
- Pas d'authentification à deux facteurs ni de SSO d'entreprise.
- Analyse de CV limitée aux formats textuels. Un PDF issu d'un scan sans couche de texte n'est pas exploitable : il faudrait un moteur OCR.
- Interface en français uniquement, alors que le groupe est international.
- Fichiers .doc binaires anciens mal extraits ; les formats modernes sont couverts.
- Instance unique. SQLite convient à la volumétrie visée mais ne permet pas de répartir la charge sur plusieurs serveurs.
Chapitre 12Feuille de route
| Horizon | Chantier |
|---|---|
| Court terme | Envoi SMTP réel ; notifications aux managers ; modèles d'e-mails par entité |
| Moyen terme | SSO Microsoft Entra ID et double authentification ; connecteur SIRH pour l'onboarding des embauchés ; diffusion automatique des offres vers les jobboards et France Travail |
| Moyen terme | Portail multilingue (anglais, allemand) ; planification des entretiens synchronisée avec les agendas |
| Long terme | Bascule PostgreSQL et exécution multi-instances ; OCR des CV scannés ; analyse des écarts de traitement à des fins de non-discrimination |
Chapitre 13Accès
Comptes de démonstration
| Rôle | Identifiant | Mot de passe |
|---|---|---|
| Administrateur RH | admin@dhl.d3v-test.me | AdminDHL2026! |
| Chargé de recrutement | recruteur@dhl.d3v-test.me | Recrut2026! |
| Manager opérationnel | manager@dhl.d3v-test.me | Manager2026! |
| Lecture seule | lecteur@dhl.d3v-test.me | Lecture2026! |
Ces identifiants relèvent d'un environnement de démonstration et sont affichés sur la page de connexion. Ils devraient être supprimés avant tout usage réel.