chore: RGPD compliance audit & cleanup + UX signature salarie

- Audit RGPD complet: création RGPD_AUDIT_LOCALISATION_DONNEES.md
- Suppression complète intégration n8n (API route, hooks, env vars)
- Suppression variables Airtable (env vars)
- Confirmation GoCardless: serveurs EEE + SCC
- 8/9 services confirmés UE (89% compliance)
- Ajout message info dans modale signature salarie (scroll down)
This commit is contained in:
odentas 2025-10-23 17:32:56 +02:00
parent d992e339d7
commit 8cb366ee53
8 changed files with 246 additions and 1203 deletions

View file

@ -20,12 +20,8 @@ AWS_REGION=eu-west-3
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
STRUCTURE_API_TOKEN=your-api-token
AIRTABLE_BASE_ID=your-base-id
AIRTABLE_API_KEY=your-airtable-pat
AIRTABLE_TABLE_CONTRATS=Contrats de travail
UPSTREAM_API_BASE=https://your-api-gateway.amazonaws.com/default
UPSTREAM_API_PREFIX=
N8N_NOTES_WEBHOOK_URL=https://your-n8n.com/webhook/...
# DocuSeal direct API (recommended)
DOCUSEAL_API_BASE=https://api.docuseal.com

View file

@ -1,12 +1,5 @@
NEXT_PUBLIC_API_BASE=https://api-dev.odentas.fr
AIRTABLE_BASE_ID=
AIRTABLE_API_KEY=
AIRTABLE_TABLE_CONTRATS=Contrats de travail
DOCUSEAL_API_BASE=
DOCUSEAL_TOKEN=
# Optional fallback
# DOCUSEAL_PROXY_BASE=
# Webhook n8n (virement effectué → mise à jour Airtable)
# Exemple: https://n8n.example.com/webhook/virement-effectue
N8N_VIREMENT_WEBHOOK_URL=

View file

@ -0,0 +1,235 @@
# 🇪🇺 Audit RGPD - Localisation des données clients
> **Date de l'audit** : 23 octobre 2025
> **Projet** : Nouvel Espace Paie Odentas
> **Objectif** : Vérifier que toutes les données clients restent dans l'Union Européenne
---
## 📊 Tableau de conformité RGPD
| Service | Localisation | Infrastructure | Statut RGPD | Données transférées | Action requise |
|---------|--------------|----------------|-------------|---------------------|----------------|
| **AWS S3** | 🇪🇺 eu-west-3 (Paris) | AWS | ✅ Conforme | Documents, logs emails | - |
| **AWS SES** | 🇪🇺 eu-west-3 (Paris) | AWS | ✅ Conforme | Envoi emails | - |
| **AWS Lambda** | 🇪🇺 eu-west-3 (Paris) | AWS API Gateway | ✅ Conforme | Processing données | - |
| **Supabase** | 🇪🇺 EU | Cloudflare + serveurs UE | ✅ Conforme | Base de données complète | - |
| **Vercel Functions** | 🇪🇺 cdg1 (Paris) | Vercel Edge | ✅ Conforme | API routes, exécution | - |
| **Docuseal** | 🇪🇺 api.docuseal.eu | Version EU | ✅ Conforme | Signatures électroniques | - |
| **PostHog** | 🇪🇺 eu.i.posthog.com | Instance EU | ✅ Conforme | Analytics utilisateurs | - |
| **GoCardless** | 🇪🇺 EEE | Serveurs EEE + SCC UE | ✅ Conforme | Mandats SEPA, paiements | ✅ **Confirmé par support** |
| **PDFMonkey** | ⚠️ **En attente** | Heroku + AWS (région ?) | ⚠️ **À confirmer** | Contrats CDDU (données salariés) | **Mail envoyé - en attente réponse** |
---
## 🔍 Détails par service
### ✅ Services conformes RGPD
#### AWS (S3, SES, Lambda)
- **Région** : `eu-west-3` (Paris, France)
- **Configuration** : `AWS_REGION=eu-west-3`
- **Données stockées** :
- S3 bucket `odentas-docs` : Documents PDF, contrats, fiches de paie
- S3 bucket `stockage-logs-emails` : Logs d'envoi d'emails
- Lambda : Processing des données via API Gateway
- **Statut** : ✅ **100% conforme**
#### Supabase
- **Instance** : `fusqtpjififcmgbhmosq.supabase.co`
- **Infrastructure** : Serveurs EU via Cloudflare
- **Données stockées** : Base de données complète (profils, organisations, salariés, contrats, etc.)
- **Statut** : ✅ **100% conforme**
#### Vercel
- **Région Functions** : `cdg1` (Paris, France)
- **Configuration** : `vercel.json``"regions": ["cdg1"]`
- **Important** : Les builds se font aux USA mais **aucune donnée client** n'y transite
- Le code source est compilé aux USA
- Le code compilé est déployé sur cdg1
- Les données clients ne quittent jamais cdg1
- **Statut** : ✅ **100% conforme**
#### Docuseal
- **API** : `https://api.docuseal.eu`
- **Version** : Européenne (`.eu`)
- **Données** : Signatures électroniques (contrats, avenants)
- **Statut** : ✅ **100% conforme**
#### PostHog
- **Instance** : `https://eu.i.posthog.com`
- **Configuration** :
```env
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
```
- **Données** : Analytics utilisateurs (événements, pages vues)
- **Statut** : ✅ **100% conforme**
---
### ⚠️ Services à vérifier
#### PDFMonkey
- **API** : `https://api.pdfmonkey.io`
- **Entreprise** : Française (Paris - 51 rue de Ponthieu, 75008)
- **Infrastructure déclarée** : Heroku + AWS (région non spécifiée)
- **Données envoyées** :
- Informations contrats CDDU (nom, prénom, dates, salaires)
- Informations structure employeur
- Informations production
- **Problème** : Heroku héberge par défaut aux USA (`us-east-1`)
- **Action** :
- ✅ Mail envoyé à `tinymonkey@pdfmonkey.io` le 23/10/2025
- ⏳ En attente de confirmation de la région de traitement
- **Statut** : ⚠️ **En attente de réponse**
#### GoCardless
- **Environment** : `live`
- **API** : GoCardless UK/EU
- **Infrastructure** : Serveurs dans l'Espace Économique Européen (EEE)
- **Données** : Mandats SEPA, paiements, coordonnées bancaires
- **Mécanismes de protection** :
- Opérations principales de traitement des paiements : ✅ Serveurs EEE
- Prestataires tiers (hors EEE) : ✅ Clauses Contractuelles Types (SCC) de l'UE
- Contrôle préalable des fournisseurs avec mécanismes RGPD approuvés
- **Documentation** :
- https://gocardless.com/legal/data-protection/
- https://gocardless.com/fr-fr/privacy/fr-gdpr/
- **Confirmation** : ✅ Support GoCardless - 23 octobre 2025
- **Statut** : ✅ **Conforme RGPD**
---
## 📝 Notes sur les services retirés
### 🚨 Priorité CRITIQUE
1. **PDFMonkey - Attendre réponse**
- ✅ Mail envoyé le 23/10/2025
- ⏳ Attendre confirmation de la localisation des serveurs
- Si réponse négative (serveurs hors EU) → **Migrer vers alternative** :
- DocRaptor (option EU disponible)
- PDF.co (serveurs EU)
- Solution auto-hébergée (Puppeteer + AWS Lambda eu-west-3)
---
## 📝 Notes sur les services retirés
### Airtable et n8n
Ces services ont été **complètement retirés** du projet le 23 octobre 2025 :
#### Actions effectuées :
- ✅ Suppression de l'API route `/api/contrats/[id]/virement`
- ✅ Suppression du hook `useToggleVirement` dans les pages contrats
- ✅ Suppression des variables d'environnement n8n de `.env.local`
- ✅ Suppression des variables d'environnement Airtable de `.env.local`
- ✅ Nettoyage des fichiers `.env.example` et `.env.local.example`
#### Résidus restants (inactifs) :
- Fichiers de backup : `.env.local.bak`, `.env.local.bak2`
- Fichier HTML standalone : `hub_signature_batch.html`
- Fichiers de documentation : `VIREMENTS_SALAIRES_STAFF_*.md`
- Commentaire dans `app/api/access/route.ts` (ligne 412)
- Lambda AWS ancienne : `/tmp/aws-toolkit-vscode/lambda/.../postDocuSealAvenantPDF/index.js`
**Impact RGPD** : ✅ **Aucun** - Aucune donnée n'est envoyée à ces services
---
## 🎯 Plan d'action
### 🚨 Priorité CRITIQUE
1. **PDFMonkey - Attendre réponse**
- ✅ Mail envoyé le 23/10/2025
- ⏳ Attendre confirmation de la localisation des serveurs
- Si réponse négative (serveurs hors EU) → **Migrer vers alternative** :
- DocRaptor (option EU disponible)
- PDF.co (serveurs EU)
- Solution auto-hébergée (Puppeteer + AWS Lambda eu-west-3)
---
## 📝 Emails et confirmations
### Email envoyé à PDFMonkey
**Date** : 23 octobre 2025
**Destinataire** : `tinymonkey@pdfmonkey.io`
**Objet** : Question sur la localisation des données (RGPD)
```
Bonjour,
Nous utilisons PDFMonkey pour générer des PDF contenant des données personnelles
de salariés (contrats de travail CDDU) et devons nous assurer de la conformité RGPD.
Pouvez-vous nous confirmer dans quelle région AWS/Heroku sont hébergées et
traitées les données que nous vous envoyons via l'API ?
Spécifiquement :
- Les données restent-elles dans l'Union Européenne ?
- Quelle est la région de vos serveurs de production (Heroku et AWS) ?
Merci d'avance pour votre retour rapide.
```
### Réponse de GoCardless
**Date** : 23 octobre 2025
**Source** : Support GoCardless
**Statut** : ✅ **Conforme RGPD**
**Résumé de la réponse** :
- ✅ **Opérations principales** : Serveurs dans l'Espace Économique Européen (EEE)
- ✅ **Prestataires tiers** : Clauses Contractuelles Types (SCC) de l'Union européenne
- ✅ **Contrôle préalable** : Mécanismes approuvés par le RGPD (constat d'adéquation, règles d'entreprise contraignantes)
- ✅ **Protection des données** : Normes de l'Union européenne respectées
**Documentation** :
- Politique RGPD : https://gocardless.com/fr-fr/privacy/fr-gdpr/
- Protection des données : https://gocardless.com/legal/data-protection/
**Citation** :
> "L'ensemble de nos principales opérations de traitement des paiements européens sont exécutées sur des serveurs situés dans l'Espace économique européen (EEE). [...] Dès lors que des données sont stockées dans ces services, nous veillons à ce qu'elles soient protégées selon les normes de l'Union européenne, en utilisant un mécanisme de transfert approuvé par le RGPD."
---
## 📝 Notes importantes
### Concernant Vercel
Les builds Vercel aux USA **n'ont AUCUN impact** sur les données clients :
- Le **code source** est compilé aux USA
- Le **code compilé** est déployé sur `cdg1` (Paris)
- Les **API Functions** s'exécutent sur `cdg1`
- Les **données clients** ne transitent **JAMAIS** par les serveurs de build
- Vercel ne stocke pas de données clients, uniquement des logs techniques
**Aucun risque RGPD** de ce côté.
### CDNs externes
Plusieurs CDNs sont utilisés pour charger des bibliothèques JavaScript :
- `cdn.docuseal.com` (formulaires de signature)
- `cdn.jsdelivr.net` (Bootstrap, Flatpickr)
- `cdnjs.cloudflare.com` (Font Awesome)
Ces CDNs peuvent servir depuis des serveurs hors UE, mais ils ne contiennent **aucune donnée client**, uniquement des fichiers statiques (CSS, JS, fonts).
**Pas de problème RGPD** : Seuls des assets publics transitent, pas de données personnelles.
---
## 📊 Résumé
| Statut | Nombre de services |
|--------|-------------------|
| ✅ Conforme RGPD | 8 services |
| ⚠️ À vérifier | 1 service |
| ❌ Non conforme | 0 service |
| ✅ Retiré | 2 services (Airtable, n8n) |
### Taux de conformité actuel
**8 / 9 confirmés = 89%**
Avec la vérification en cours (PDFMonkey), le taux devrait atteindre **100%**.

View file

@ -406,30 +406,6 @@ function useContratDetail(id: string) {
});
}
// Exemple mutation (toggle virement). Adapte au vrai endpoint si différent.
function useToggleVirement(id: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (params: { value: boolean; contractRef?: string }) => {
const { value, contractRef } = params;
// Normaliser en "Oui" / "Non" pour l'écosystème (Airtable via n8n)
const status = value ? "Oui" : "Non";
const res = await fetch(`/api/contrats/${id}/virement`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include",
body: JSON.stringify({ virement_effectue: status, contract_ref: contractRef || undefined }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(t || `HTTP ${res.status}`);
}
return (await res.json()) as { ok: boolean };
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["contrat", id] }),
});
}
// ---------- UI helpers ----------
function Section({ title, icon: Icon, children }: { title: string; icon?: React.ElementType; children: React.ReactNode }) {
return (
@ -862,8 +838,6 @@ export default function ContratPage() {
}
fetchSignedUrls();
}, [payslipsQuery.data]);
const toggleMut = useToggleVirement(id);
// Redirection de sécurité côté client
useEffect(() => {

View file

@ -305,30 +305,6 @@ function useContratDetail(id: string) {
// Pas de mode normal : cette page est TOUJOURS en mode démo
}
// Exemple mutation (toggle virement). Adapte au vrai endpoint si différent.
function useToggleVirement(id: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (params: { value: boolean; contractRef?: string }) => {
const { value, contractRef } = params;
// Normaliser en "Oui" / "Non" pour l'écosystème (Airtable via n8n)
const status = value ? "Oui" : "Non";
const res = await fetch(`/api/contrats/${id}/virement`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include",
body: JSON.stringify({ virement_effectue: status, contract_ref: contractRef || undefined }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(t || `HTTP ${res.status}`);
}
return (await res.json()) as { ok: boolean };
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["contrat", id] }),
});
}
// ---------- UI helpers ----------
function Section({ title, icon: Icon, children }: { title: string; icon?: React.ElementType; children: React.ReactNode }) {
return (
@ -781,9 +757,6 @@ export default function ContratDemoPage() {
// fetchSignedUrls();
// }, [payslipsQuery.data]);
const toggleMut = useToggleVirement(id);
// Redirection de sécurité côté client
useEffect(() => {
// Si l'utilisateur essaie d'accéder à un ID suspect, on peut ajouter des vérifications

View file

@ -1,73 +0,0 @@
// app/api/contrats/[id]/virement/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export const runtime = "nodejs";
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
const contractId = params?.id;
if (!contractId) return NextResponse.json({ error: "missing_id" }, { status: 400 });
const supabase = createRouteHandlerClient({ cookies });
const { data: { session } } = await supabase.auth.getSession();
if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
let body: any = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}
// Normaliser en "Oui" / "Non"
const raw = body?.virement_effectue;
const normalized = ((): "Oui" | "Non" => {
if (typeof raw === "string") {
const s = raw.trim().toLowerCase();
return s === "oui" || s === "true" || s === "1" ? "Oui" : "Non";
}
return raw ? "Oui" : "Non";
})();
const contract_ref = String(body?.contract_ref || "");
const webhookUrl = process.env.N8N_VIREMENT_WEBHOOK_URL || process.env.N8N_WEBHOOK_VIREMENT_URL;
if (!webhookUrl) {
// Pas de webhook configuré, mais on répond OK pour ne pas bloquer l'UI
console.warn(" N8N webhook URL not configured (N8N_VIREMENT_WEBHOOK_URL)");
return NextResponse.json({ ok: true, skipped: true });
}
const payload = {
contract_id: contractId,
contract_ref: contract_ref || null,
virement_effectue: normalized, // "Oui" / "Non"
// Contexte utilisateur minimal
user_id: session.user.id,
user_email: session.user.email,
triggered_at: new Date().toISOString(),
};
try {
const res = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
cache: "no-store",
});
if (!res.ok) {
const text = await res.text().catch(() => "");
console.error("❌ N8N webhook error:", res.status, text);
return NextResponse.json({ ok: false, error: "webhook_failed", status: res.status }, { status: 502 });
}
} catch (e: any) {
console.error("💥 N8N webhook call threw:", e?.message || e);
return NextResponse.json({ ok: false, error: "webhook_exception" }, { status: 502 });
}
return NextResponse.json({ ok: true });
}

View file

@ -149,9 +149,17 @@ export default function DocuSealSignatureModal({
<div className="bg-white rounded-2xl h-full flex flex-col max-h-[90vh]">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b bg-gradient-to-r from-blue-50 to-indigo-50 flex-shrink-0">
<h2 className="text-xl font-semibold text-slate-900">
Signature électronique du contrat
</h2>
<div className="flex-1">
<h2 className="text-xl font-semibold text-slate-900">
Signature électronique du contrat
</h2>
<p className="text-xs text-slate-600 mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Descendez tout en bas du contrat pour accéder au champs de signature.
</p>
</div>
<button
onClick={() => {
if (!showCompletedModal) {

File diff suppressed because it is too large Load diff