Pages mentions légales et politique de confidentialité

Nouvelles pages publiques /mentions-legales et /confidentialite,
conformes aux obligations d'une association loi 1901 : LCEN (identité de
l'éditeur, RNA, directeur de la publication, hébergeur), RGPD art. 13
(responsable de traitement, données/finalités/bases légales par
formulaire, durées de conservation, destinataires, droits, réclamation
CNIL), cookies (unique cookie de session exempté de consentement — pas
de bandeau nécessaire), droit à l'image et propriété intellectuelle.

Les pages sont alimentées par les paramètres de l'association existants,
étendus des champs légalement requis : numéro RNA, hébergeur (nom,
adresse, téléphone) et date de mise à jour des pages légales — nouvelle
carte « Pages légales & hébergeur » dans l'onglet Paramètres, avec
alerte listant les champs requis manquants. Un champ vide s'affiche
« à compléter » sur la page publique plutôt que de disparaître.

Également : liens cliquables Mentions légales / Confidentialité dans le
pied de page, et mentions d'information RGPD sous les formulaires
publics (demande d'adhésion, inscription à une rencontre).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
Claude committed 2026-07-16 18:29:14 +00:00
1 parent 5ab7e48945
commit bdaf6cb156
10 files changed
+360 -15

No files matched your search

+6
View File
@@ -114,6 +114,12 @@ export const contentSchema = z.object({
association_treasurer: trimmed(160).optional(),
association_secretary: trimmed(160).optional(),
association_board_members: trimmed(3000).optional(),
// Legal pages (mentions légales & politique de confidentialité)
association_rna: trimmed(30).optional(),
association_host_name: trimmed(200).optional(),
association_host_address: trimmed(300).optional(),
association_host_tel: trimmed(30).optional(),
legal_updated: trimmed(60).optional(),
});
// Invitation email composer: every text zone is optional — absent fields
+6
View File
@@ -13,6 +13,12 @@ export const ASSOCIATION_DEFAULTS = {
association_treasurer: '',
association_secretary: '',
association_board_members: '',
// Legal pages (mentions légales & politique de confidentialité)
association_rna: '',
association_host_name: '',
association_host_address: '',
association_host_tel: '',
legal_updated: 'Juillet 2026',
};
export function associationSettings(content = {}) {
+4
View File
@@ -7,6 +7,8 @@ import Inscription from './pages/Inscription.jsx';
import Annuaire from './pages/Annuaire.jsx';
import Association from './pages/Association.jsx';
import RencontresPassees from './pages/RencontresPassees.jsx';
import MentionsLegales from './pages/MentionsLegales.jsx';
import Confidentialite from './pages/Confidentialite.jsx';
import Espace from './pages/Espace.jsx';
import Admin from './pages/Admin.jsx';
import { useAuth } from './lib/AuthContext.jsx';
@@ -38,6 +40,8 @@ export default function App() {
<Route path="/annuaire" element={<Annuaire />} />
<Route path="/rencontres-passees" element={<RencontresPassees />} />
<Route path="/association" element={<Association />} />
<Route path="/mentions-legales" element={<MentionsLegales />} />
<Route path="/confidentialite" element={<Confidentialite />} />
<Route path="/espace-membre" element={<Espace />} />
<Route path="/admin/*" element={<Admin />} />
<Route path="*" element={<Home />} />
+6
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../lib/api.js';
import Modal from './Modal.jsx';
@@ -66,6 +67,11 @@ export function AdhesionForm({ showTitle = true }) {
<button type="submit" className="btn btn-red" style={{ width: '100%', marginTop: 24, padding: 15 }} disabled={busy}>
{busy ? 'Envoi…' : 'Envoyer ma demande'}
</button>
<p style={{ fontSize: 11, color: 'var(--gray-light)', textAlign: 'center', marginTop: 12, lineHeight: 1.5 }}>
Les informations recueillies servent uniquement à instruire votre demande d'adhésion.
Pour en savoir plus et exercer vos droits, consultez notre{' '}
<Link to="/confidentialite" style={{ color: 'var(--red)' }}>politique de confidentialité</Link>.
</p>
</form>
);
}
+5 -1
View File
@@ -86,7 +86,11 @@ export default function Footer() {
}}
>
<span>© {new Date().getFullYear()} {settings.association_name}</span>
<span>Mentions légales · Confidentialité</span>
<span>
<Link to="/mentions-legales" style={{ color: 'inherit', textDecoration: 'none' }}>Mentions légales</Link>
{' · '}
<Link to="/confidentialite" style={{ color: 'inherit', textDecoration: 'none' }}>Confidentialité</Link>
</span>
</div>
</footer>
);
+71 -14
View File
@@ -229,6 +229,16 @@ export function ParametresTab() {
}
};
// Fields legally required on the mentions légales page (LCEN).
const legalMissing = [
[settings.association_name, "nom de l'association"],
[settings.association_address, 'adresse du siège'],
[settings.association_rna, 'numéro RNA'],
[settings.association_president, 'président·e'],
[settings.association_host_name, "nom de l'hébergeur"],
[settings.association_host_address, "adresse de l'hébergeur"],
].filter(([v]) => !String(v || '').trim()).map(([, label]) => label);
return (
<form onSubmit={save} style={{ maxWidth: 980, display: 'flex', flexDirection: 'column', gap: 20 }}>
<div className="card" style={{ borderRadius: 6, padding: 26 }}>
@@ -251,20 +261,35 @@ export function ParametresTab() {
<input name="association_phone" type="tel" value={settings.association_phone} onChange={onChange} maxLength={40} />
</label>
</div>
<label className="field">SIRET
<input
name="association_siret"
value={settings.association_siret}
onChange={onChange}
inputMode="numeric"
maxLength={20}
placeholder="14 chiffres"
aria-describedby="association-siret-help"
/>
<span id="association-siret-help" style={{ marginTop: 5, fontSize: 11.5, color: 'var(--gray-light)', fontWeight: 400 }}>
Les espaces sont acceptés; le numéro doit contenir exactement 14 chiffres.
</span>
</label>
<div className="grid-2" style={{ gap: 12 }}>
<label className="field">Numéro RNA
<input
name="association_rna"
value={settings.association_rna}
onChange={onChange}
maxLength={30}
placeholder="W543001234"
aria-describedby="association-rna-help"
/>
<span id="association-rna-help" style={{ marginTop: 5, fontSize: 11.5, color: 'var(--gray-light)', fontWeight: 400 }}>
Sur le récépissé de déclaration en préfecture — requis dans les mentions légales.
</span>
</label>
<label className="field">SIRET
<input
name="association_siret"
value={settings.association_siret}
onChange={onChange}
inputMode="numeric"
maxLength={20}
placeholder="14 chiffres"
aria-describedby="association-siret-help"
/>
<span id="association-siret-help" style={{ marginTop: 5, fontSize: 11.5, color: 'var(--gray-light)', fontWeight: 400 }}>
Les espaces sont acceptés; le numéro doit contenir exactement 14 chiffres.
</span>
</label>
</div>
<div className="grid-2" style={{ gap: 12 }}>
<label className="field">Contact principal
<input name="association_contact" value={settings.association_contact} onChange={onChange} maxLength={160} placeholder="Nom et fonction" />
@@ -319,6 +344,38 @@ export function ParametresTab() {
</div>
</div>
<div className="card" style={{ borderRadius: 6, padding: 26 }}>
<h3 className="serif" style={{ fontSize: 19, fontWeight: 600, marginBottom: 8 }}>Pages légales & hébergeur</h3>
<p style={{ fontSize: 13.5, color: 'var(--gray-light)', lineHeight: 1.55, marginBottom: 14 }}>
Ces informations alimentent les pages <strong>Mentions légales</strong> et{' '}
<strong>Politique de confidentialité</strong> (liens en pied de page). La LCEN impose
d'identifier l'hébergeur du site ; tant qu'un champ requis est vide, la page publique
affiche « à compléter » à sa place.
</p>
{legalMissing.length > 0 && (
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '12px 16px', background: '#FBF6EE', border: '1px solid #F0D6BC', borderRadius: 6, fontSize: 12.5, color: '#8A6D3B', lineHeight: 1.5, marginBottom: 18 }}>
<span>⚠</span>
<span>Champs requis manquants pour des mentions légales conformes : {legalMissing.join(', ')}.</span>
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<label className="field">Hébergeur — nom / raison sociale
<input name="association_host_name" value={settings.association_host_name} onChange={onChange} maxLength={200} placeholder="Ex. OVH SAS, Scaleway, ou vous-même si auto-hébergé" />
</label>
<label className="field">Hébergeur — adresse
<input name="association_host_address" value={settings.association_host_address} onChange={onChange} maxLength={300} placeholder="Ex. 2 rue Kellermann, 59100 Roubaix, France" />
</label>
<div className="grid-2" style={{ gap: 12 }}>
<label className="field">Hébergeur — téléphone
<input name="association_host_tel" value={settings.association_host_tel} onChange={onChange} maxLength={30} />
</label>
<label className="field">Date de mise à jour des pages légales
<input name="legal_updated" value={settings.legal_updated} onChange={onChange} maxLength={60} placeholder="Ex. Juillet 2026" />
</label>
</div>
</div>
</div>
{error && <p className="error-text">{error}</p>}
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<button type="submit" className="btn btn-red btn-sm" style={{ fontSize: 14, padding: '12px 22px' }}>Enregistrer les paramètres</button>
+6
View File
@@ -13,6 +13,12 @@ export const ASSOCIATION_DEFAULTS = {
association_treasurer: '',
association_secretary: '',
association_board_members: '',
// Legal pages (mentions légales & politique de confidentialité)
association_rna: '',
association_host_name: '',
association_host_address: '',
association_host_tel: '',
legal_updated: 'Juillet 2026',
};
export function associationSettings(content = {}) {
+135
View File
@@ -0,0 +1,135 @@
import { usePublicData } from '../lib/usePublic.js';
import { associationSettings } from '../lib/siteSettings.js';
import { LegalLayout, LegalSection, LegalValue } from './MentionsLegales.jsx';
// Privacy policy (art. 13 RGPD). The described processing matches exactly
// what this application actually does — update this page if a new
// collection or a third-party service (analytics, newsletter…) is added.
export default function Confidentialite() {
const { data } = usePublicData();
const s = associationSettings(data?.content);
const email = s.association_email;
const row = { display: 'grid', gridTemplateColumns: '190px 1fr', gap: 14, padding: '12px 0', borderBottom: '1px solid var(--border-soft)', fontSize: 14.5, lineHeight: 1.6 };
const cellTitle = { fontWeight: 600, color: 'var(--ink)' };
return (
<LegalLayout kicker="Informations légales" title="Politique de confidentialité" updated={s.legal_updated}>
<LegalSection title="Responsable de traitement">
<p>
Le responsable du traitement des données personnelles collectées sur ce site est{' '}
<strong><LegalValue value={s.association_name} label="nom de l'association" /></strong>, association loi 1901,{' '}
<LegalValue value={s.association_address} label="adresse du siège" multiline />.
<br />
Contact : <LegalValue value={email} label="email de contact" />
{s.association_phone && <> · {s.association_phone}</>}
</p>
</LegalSection>
<LegalSection title="Données collectées et finalités">
<p>Nous ne collectons que les données strictement nécessaires au fonctionnement du club :</p>
<div className="card" style={{ borderRadius: 6, padding: '6px 22px', marginTop: 4 }}>
<div style={row}>
<span style={cellTitle}>Demande d'adhésion</span>
<span>
Nom et prénom, fonction, entreprise, email professionnel, téléphone.
<br /><em>Finalité :</em> instruire votre demande et vous recontacter. <em>Base légale :</em> mesures précontractuelles à votre demande.
</span>
</div>
<div style={row}>
<span style={cellTitle}>Inscription à une rencontre</span>
<span>
Noms et prénoms des participants, rattachés au compte membre qui les inscrit.
<br /><em>Finalité :</em> gérer les inscriptions et l'accueil des participants. <em>Base légale :</em> exécution de l'adhésion.
</span>
</div>
<div style={row}>
<span style={cellTitle}>Annuaire des membres</span>
<span>
Fiche entreprise : nom, secteur, dirigeant·e, coordonnées professionnelles, présentation, logo, photo du dirigeant·e.
<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, borderBottom: 'none' }}>
<span style={cellTitle}>Espace membre</span>
<span>
Email de connexion et mot de passe (stocké haché, jamais en clair).
<br /><em>Finalité :</em> accès sécurisé à votre espace. <em>Base légale :</em> exécution de l'adhésion.
</span>
</div>
</div>
<p style={{ fontSize: 13.5, color: 'var(--gray-light)' }}>
Aucune donnée n'est utilisée à des fins publicitaires, ni cédée ou vendue à des tiers.
Le site n'utilise aucun outil de mesure d'audience ni traceur tiers.
</p>
</LegalSection>
<LegalSection title="Durées de conservation">
<p>
Les données des membres sont conservées pendant la durée de l'adhésion, puis au maximum
3 ans après son terme (animation du réseau et relance d'adhésion), avant suppression.
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.
</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.
</p>
<p>
Les données sont hébergées par <LegalValue value={s.association_host_name} label="nom de l'hébergeur" />,{' '}
<LegalValue value={s.association_host_address} label="adresse de l'hébergeur" />. Aucun transfert de
données hors de l'Union européenne n'est réalisé.
</p>
</LegalSection>
<LegalSection title="Cookies">
<p>
Le site dépose un unique cookie de session, strictement nécessaire à l'authentification à
l'espace membre et à l'administration (cookie technique httpOnly, supprimé à l'expiration de la
session). Conformément aux lignes directrices de la CNIL, ce cookie est exempté de consentement.
Aucun cookie publicitaire, de mesure d'audience ou de réseau social n'est utilisé — c'est
pourquoi ce site n'affiche pas de bandeau cookies.
</p>
</LegalSection>
<LegalSection title="Sécurité">
<p>
L'association met en œuvre des mesures techniques et organisationnelles adaptées : mots de
passe stockés sous forme hachée, connexions chiffrées, accès à l'administration restreint et
protégé, validation des fichiers déposés. En cas de violation de données susceptible
d'engendrer un risque pour vos droits, vous en serez informé·e conformément à la réglementation.
</p>
</LegalSection>
<LegalSection title="Droit à l'image">
<p>
Des photographies sont prises lors des rencontres du club et peuvent être publiées sur ce site
(section « Rencontres passées »). Elles ne sont publiées qu'avec l'accord des personnes
identifiables. Si vous apparaissez sur une photographie et souhaitez son retrait, écrivez à{' '}
<LegalValue value={email} label="email de contact" /> : elle sera retirée dans les meilleurs délais.
</p>
</LegalSection>
<LegalSection title="Vos droits">
<p>
Conformément au Règlement général sur la protection des données (RGPD) et à la loi
Informatique et Libertés, vous disposez d'un droit d'accès, de rectification, d'effacement,
de limitation, d'opposition et de portabilité sur vos données.
</p>
<p>
Pour exercer ces droits, contactez-nous à <LegalValue value={email} label="email de contact" />.
Une réponse vous sera apportée dans un délai d'un mois. Si vous estimez, après nous avoir
contactés, que vos droits ne sont pas respectés, vous pouvez adresser une réclamation à la
CNIL (<a href="https://www.cnil.fr" target="_blank" rel="noopener noreferrer" className="btn-link" style={{ fontSize: 'inherit' }}>www.cnil.fr</a> —
3 place de Fontenoy, TSA 80715, 75334 Paris Cedex 07).
</p>
</LegalSection>
</LegalLayout>
);
}
+4
View File
@@ -157,6 +157,10 @@ export function InscriptionModal({ rencontre, onClose, onDone }) {
<p style={{ fontSize: 12, color: 'var(--gray-light)', textAlign: 'center', marginTop: 12, lineHeight: 1.5 }}>
Vous pourrez rouvrir ce formulaire pour modifier les noms enregistrés.
</p>
<p style={{ fontSize: 11, color: 'var(--gray-light)', textAlign: 'center', marginTop: 8, lineHeight: 1.5 }}>
Les noms recueillis servent uniquement à gérer les inscriptions à cette rencontre —{' '}
<Link to="/confidentialite" style={{ color: 'var(--red)' }}>politique de confidentialité</Link>.
</p>
</form>
)}
</Modal>
+117
View File
@@ -0,0 +1,117 @@
import { Link } from 'react-router-dom';
import { usePublicData } from '../lib/usePublic.js';
import { associationSettings } from '../lib/siteSettings.js';
// Shared bits for the two legal pages. Values come from the association
// parameters maintained in the admin backend (Paramètres) ; an empty
// value renders a visible "à compléter" marker so a missing legal mention
// never fails silently.
export function LegalValue({ value, label, multiline = false }) {
if (value) {
return multiline ? <span style={{ whiteSpace: 'pre-line' }}>{value}</span> : <>{value}</>;
}
return (
<span style={{ background: '#FBEFD9', color: '#8A5A22', padding: '1px 8px', borderRadius: 3, fontSize: '.88em', fontWeight: 600 }}>
à compléter — {label}
</span>
);
}
export function LegalLayout({ kicker, title, updated, children }) {
return (
<main>
<section style={{ background: '#fff', borderBottom: '1px solid var(--border)' }}>
<div className="container" style={{ maxWidth: 880, padding: '64px 32px 46px' }}>
<div className="kicker" style={{ marginBottom: 14 }}>{kicker}</div>
<h1 className="serif" style={{ fontWeight: 500, fontSize: 'clamp(32px, 4.5vw, 46px)', lineHeight: 1.08 }}>{title}</h1>
{updated && (
<div style={{ fontSize: 13, color: 'var(--gray-light)', marginTop: 14 }}>Dernière mise à jour : {updated}</div>
)}
</div>
</section>
<section style={{ background: 'var(--bg)', padding: '52px 0 96px' }}>
<div className="container" style={{ maxWidth: 880 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 36 }}>{children}</div>
</div>
</section>
</main>
);
}
export function LegalSection({ title, children }) {
return (
<section>
<h2 className="serif" style={{ fontSize: 24, fontWeight: 600, marginBottom: 14 }}>{title}</h2>
<div style={{ fontSize: 15, lineHeight: 1.75, color: 'var(--gray)', display: 'flex', flexDirection: 'column', gap: 10 }}>
{children}
</div>
</section>
);
}
export default function MentionsLegales() {
const { data } = usePublicData();
const s = associationSettings(data?.content);
return (
<LegalLayout kicker="Informations légales" title="Mentions légales" updated={s.legal_updated}>
<LegalSection title="Éditeur du site">
<p>
Le présent site est édité par <strong><LegalValue value={s.association_name} label="nom de l'association" /></strong>,
association régie par la loi du 1<sup>er</sup> juillet 1901 et le décret du 16 août 1901.
</p>
<p>
Siège social : <LegalValue value={s.association_address} label="adresse du siège" multiline />
<br />
Numéro RNA : <LegalValue value={s.association_rna} label="numéro RNA" />
{s.association_siret && (<><br />SIRET : {s.association_siret}</>)}
<br />
Email : <LegalValue value={s.association_email} label="email de contact" />
<br />
Téléphone : <LegalValue value={s.association_phone} label="téléphone" />
</p>
</LegalSection>
<LegalSection title="Directeur de la publication">
<p>
Le directeur de la publication est <LegalValue value={s.association_president} label="nom du président" />,
en qualité de président·e de l'association.
</p>
</LegalSection>
<LegalSection title="Hébergement">
<p>
Le site est hébergé par <LegalValue value={s.association_host_name} label="nom de l'hébergeur" />
<br />
Adresse : <LegalValue value={s.association_host_address} label="adresse de l'hébergeur" />
<br />
Téléphone : <LegalValue value={s.association_host_tel} label="téléphone de l'hébergeur" />
</p>
</LegalSection>
<LegalSection title="Propriété intellectuelle">
<p>
L'ensemble des contenus du site (textes, photographies, logos, identité visuelle) est la
propriété de l'association ou de ses partenaires, et est protégé par le droit de la propriété
intellectuelle. Toute reproduction, représentation ou diffusion, totale ou partielle, sans
autorisation écrite préalable est interdite. Les logos et marques des entreprises membres
demeurent la propriété de leurs titulaires respectifs.
</p>
</LegalSection>
<LegalSection title="Données personnelles">
<p>
Le traitement des données personnelles collectées sur ce site est décrit dans la{' '}
<Link to="/confidentialite" className="btn-link" style={{ fontSize: 'inherit' }}>politique de confidentialité</Link>.
</p>
</LegalSection>
<LegalSection title="Droit applicable">
<p>
Le présent site est soumis au droit français. En cas de litige et à défaut de résolution
amiable, les tribunaux français seront seuls compétents.
</p>
</LegalSection>
</LegalLayout>
);
}