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:
parent
d992e339d7
commit
8cb366ee53
8 changed files with 246 additions and 1203 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
235
RGPD_AUDIT_LOCALISATION_DONNEES.md
Normal file
235
RGPD_AUDIT_LOCALISATION_DONNEES.md
Normal 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%**.
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
@ -780,9 +756,6 @@ export default function ContratDemoPage() {
|
|||
// }
|
||||
// fetchSignedUrls();
|
||||
// }, [payslipsQuery.data]);
|
||||
|
||||
const toggleMut = useToggleVirement(id);
|
||||
|
||||
|
||||
// Redirection de sécurité côté client
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
@ -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
Loading…
Reference in a new issue