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:
9 files changed
+453
-6
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />,{' '}
|
||||
|
||||
Reference in new issue
Block a user