Application complète SLUC Business Club (React + Express + PostgreSQL, Docker)

Implémente l'intégralité de la maquette « SLUC Business Club.dc.html » :
- Site public : accueil (héro administrable, carrousel membres, agenda avec
  inscription en ligne, rencontres passées, demande d'adhésion), annuaire
  avec recherche/filtres/fiche détaillée, page association
- Espace membre : connexion, édition de fiche avec aperçu direct, upload
  logo/photo, statut d'adhésion par saison, changement de mot de passe
- Espace admin : tableau de bord, membres (validation par saison),
  rencontres (CRUD + inscrits + impression + export Excel), inscriptions,
  catégories, contenu du site

Architecture : 3 conteneurs Docker Compose — PostgreSQL 16 (réseau interne,
rôle applicatif restreint), API Express (non-root, read-only fs), nginx
non privilégié (frontend React + reverse-proxy + CSP stricte).

Sécurité : requêtes 100 % paramétrées, bcrypt + JWT httpOnly SameSite=Strict,
vérification d'Origin (CSRF), validation zod, rate limiting, uploads vérifiés
par octets magiques avec noms aléatoires, aucun secret committé.

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-10 19:43:07 +00:00
commit 96dca2a802
50 files changed
+6887

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
async function request(path, options = {}) {
const res = await fetch(path, {
headers: options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' },
credentials: 'same-origin',
...options,
body:
options.body instanceof FormData
? options.body
: options.body
? JSON.stringify(options.body)
: undefined,
});
let data = null;
try {
data = await res.json();
} catch {
/* non-JSON response */
}
if (!res.ok) {
const err = new Error(data?.error || `Erreur ${res.status}`);
err.status = res.status;
throw err;
}
return data;
}
export const api = {
get: (path) => request(path),
post: (path, body) => request(path, { method: 'POST', body }),
put: (path, body) => request(path, { method: 'PUT', body }),
del: (path) => request(path, { method: 'DELETE' }),
upload: (path, file) => {
const fd = new FormData();
fd.append('file', file);
return request(path, { method: 'POST', body: fd });
},
};
const MOIS = ['Jan', 'Fév', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc'];
export function dateParts(dateStr) {
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return { jour: '--', mois: '' };
return { jour: String(d.getUTCDate()).padStart(2, '0'), mois: MOIS[d.getUTCMonth()] };
}
export function seasonLabel(d = new Date()) {
const y = d.getFullYear();
return d.getMonth() >= 8 ? `${y}–${y + 1}` : `${y - 1}–${y}`;
}
export const statutLabel = (s) => (s === 'confirmee' ? 'Confirmée' : 'En attente');