Conformité RGPD : registre des traitements, droit à l'image, HTTPS

Registre des traitements (art. 30 RGPD) et formulaires d'autorisation de
droit à l'image (majeur / mineur) générés depuis un nouvel onglet « RGPD »
de l'administration (admins uniquement) : documents A4 imprimables et
téléchargeables, pré-remplis à partir des Paramètres de l'association. Le
registre recense les 7 traitements réels de l'application (adhésions,
annuaire, inscriptions, facturation, espace membre, photos, e-mails) avec
finalité, base légale, personnes, données, destinataires et durées ; la
facturation est conservée 10 ans (obligation comptable, art. L123-22).

Politique de confidentialité complétée du traitement de facturation
(adresse, factures, paiements), de la conservation comptable de 10 ans et
des destinataires (trésoriers, expert-comptable, administration fiscale).

HTTPS en production : en-tête HSTS (1 an, includeSubDomains) et redirection
HTTP→HTTPS (308) des requêtes reçues via le reverse proxy (X-Forwarded-Proto),
sans impacter les appels directs (healthcheck). Nouveau drapeau FORCE_HTTPS
dans docker-compose et .env.example, aux côtés de COOKIE_SECURE et
TRUST_PROXY documentés pour la production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
Claude committed 2026-07-16 20:30:48 +00:00
1 parent 942b5a1ad2
commit 942bc70865
9 files changed
+453 -6

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
import { useEffect, useState } from 'react';
import { api } from '../../lib/api.js';
// Admin tab grouping the association's GDPR compliance documents. Each is
// generated server-side (prefilled from the association parameters) and can
// be previewed, printed and downloaded as a standalone HTML file.
const DOCS = [
{ key: 'register', label: 'Registre des traitements', endpoint: '/api/admin/compliance/register',
hint: "Document interne obligatoire (art. 30 RGPD) recensant tous les traitements de données. À conserver et présenter à la CNIL sur demande." },
{ key: 'consent-adult', label: 'Droit à l’image — majeur', endpoint: '/api/admin/compliance/image-consent',
hint: 'Formulaire à faire signer aux participants avant de publier des photos les représentant.' },
{ key: 'consent-minor', label: 'Droit à l’image — mineur', endpoint: '/api/admin/compliance/image-consent?minor=1',
hint: 'Variante à faire signer par le représentant légal pour toute personne mineure.' },
];
export function RgpdTab() {
const [active, setActive] = useState('register');
const [data, setData] = useState(null);
const [error, setError] = useState('');
const doc = DOCS.find((d) => d.key === active);
useEffect(() => {
setData(null);
setError('');
api.get(doc.endpoint).then(setData).catch((e) => setError(e.message));
}, [active]);
const print = () => {
const w = window.open('', '_blank');
if (!w) return;
w.document.open();
w.document.write(data.html);
w.document.close();
setTimeout(() => { w.focus(); w.print(); }, 350);
};
const download = () => {
const blob = new Blob([data.html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${active}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 2000);
};
return (
<div>
<div style={{ marginBottom: 18 }}>
<h3 className="serif" style={{ fontSize: 20, fontWeight: 600 }}>Conformité RGPD</h3>
<div style={{ fontSize: 12.5, color: 'var(--gray-light)', marginTop: 3, lineHeight: 1.55, maxWidth: 720 }}>
Documents générés automatiquement à partir des <strong>Paramètres de l'association</strong>.
Complétez-y les informations manquantes (RNA, président·e, hébergeur…) pour des documents conformes.
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 16 }}>
{DOCS.map((d) => (
<button
key={d.key}
className="btn btn-sm"
onClick={() => setActive(d.key)}
style={{
fontSize: 13, padding: '10px 16px',
background: active === d.key ? 'var(--red)' : 'var(--admin-bg)',
color: active === d.key ? '#fff' : 'var(--ink)',
}}
>
{d.label}
</button>
))}
</div>
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', marginBottom: 12 }}>
<button className="btn btn-red btn-sm" style={{ fontSize: 13.5, padding: '11px 18px' }} onClick={print} disabled={!data}>
⎙ Imprimer
</button>
<button className="btn btn-outline-soft btn-sm" style={{ fontSize: 13.5, padding: '11px 18px' }} onClick={download} disabled={!data}>
↓ Télécharger (.html)
</button>
<span style={{ fontSize: 12, color: 'var(--gray-light)', lineHeight: 1.5, flex: 1, minWidth: 240 }}>{doc.hint}</span>
</div>
{error && <p className="error-text">{error}</p>}
{!data && !error && <p style={{ color: 'var(--gray-light)' }}>Génération du document…</p>}
{data && (
<div style={{ border: '1px solid var(--border)', borderRadius: 6, overflow: 'hidden' }}>
<iframe title="Aperçu du document" srcDoc={data.html} style={{ display: 'block', width: '100%', height: 680, border: 'none', background: '#f0eee9' }} />
</div>
)}
</div>
);
}
+3
View File
@@ -9,6 +9,7 @@ import { UsersTab } from '../components/admin/AdminUsers.jsx';
import { EmailCreatorTab } from '../components/admin/AdminEmail.jsx';
import { RequestsTab } from '../components/admin/AdminRequests.jsx';
import { BillingTab } from '../components/admin/AdminBilling.jsx';
import { RgpdTab } from '../components/admin/AdminRgpd.jsx';
// Moderators get members/membership requests/rencontres/inscriptions/rencontres passées
// (create + edit, never delete) — everything else (dashboard, taxonomy,
@@ -25,6 +26,7 @@ const TABS = [
{ key: 'categories', icon: '☲', label: 'Catégories', title: 'Catégories', roles: ['admin'] },
{ key: 'contenu', icon: '▧', label: 'Contenu du site', title: 'Contenu du site', roles: ['admin'] },
{ key: 'parametres', icon: '⚙', label: 'Paramètres', title: "Paramètres de l'association", roles: ['admin'] },
{ key: 'rgpd', icon: '⚖', label: 'RGPD', title: 'Conformité RGPD', roles: ['admin'] },
{ key: 'emails', icon: '✉', label: "Créateur d'e-mail", title: "Créateur d'e-mail", roles: ['admin'] },
{ key: 'utilisateurs', icon: '⚿', label: 'Utilisateurs', title: 'Administrateurs, trésoriers & modérateurs', roles: ['admin'] },
];
@@ -161,6 +163,7 @@ export function AdminShell() {
{tab === 'categories' && <CategoriesTab />}
{tab === 'contenu' && <ContenuTab />}
{tab === 'parametres' && <ParametresTab />}
{tab === 'rgpd' && <RgpdTab />}
{tab === 'emails' && <EmailCreatorTab />}
{tab === 'utilisateurs' && <UsersTab />}
</div>
+13 -4
View File
@@ -50,6 +50,13 @@ export default function Confidentialite() {
<br /><em>Finalité :</em> annuaire public des entreprises membres. <em>Base légale :</em> exécution de l'adhésion ; la photo est publiée avec le consentement de la personne, qui peut la retirer à tout moment depuis son espace membre.
</span>
</div>
<div style={row}>
<span style={cellTitle}>Facturation des adhésions</span>
<span>
Identité, adresse de facturation, email, montants, moyen et date de paiement, numéro de facture.
<br /><em>Finalité :</em> émettre les factures d'adhésion, suivre les paiements et tenir la comptabilité. <em>Base légale :</em> obligation légale (comptable et fiscale) et exécution de l'adhésion.
</span>
</div>
<div style={{ ...row, borderBottom: 'none' }}>
<span style={cellTitle}>Espace membre</span>
<span>
@@ -71,15 +78,17 @@ export default function Confidentialite() {
Les demandes d'adhésion non abouties sont supprimées au plus tard 3 ans après leur dépôt.
Les inscriptions aux rencontres sont conservées le temps de la saison en cours puis archivées
à des fins de statistiques de fréquentation (nombre de participants uniquement).
Les comptes de connexion sont supprimés avec la fiche du membre.
Les factures et pièces comptables sont conservées 10 ans, conformément aux obligations
comptables et fiscales. Les comptes de connexion sont supprimés avec la fiche du membre.
</p>
</LegalSection>
<LegalSection title="Destinataires et hébergement">
<p>
Les données sont accessibles aux seuls administrateurs et modérateurs habilités de
l'association, soumis à une obligation de confidentialité. Les fiches de l'annuaire et les
photographies des rencontres sont, elles, publiées sur le site public.
Les données sont accessibles aux seuls administrateurs, trésoriers et modérateurs habilités de
l'association, soumis à une obligation de confidentialité ; les données comptables peuvent être
communiquées à l'expert-comptable et à l'administration fiscale le cas échéant. Les fiches de
l'annuaire et les photographies des rencontres sont, elles, publiées sur le site public.
</p>
<p>
Les données sont hébergées par <LegalValue value={s.association_host_name} label="nom de l'hébergeur" />,{' '}