diff --git a/README.md b/README.md index 7af7254..8d0ea95 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Deux conteneurs orchestrés par Docker Compose, sur des ports peu utilisés | Service | Rôle | Port hôte | |---------|------|-----------| | `app` | Node.js 22 / Express : API REST **et** frontend React compilé **et** images `/uploads` | `8321` (`APP_PORT`) | -| `db` | PostgreSQL 16 (schéma + données de démo au premier démarrage) | `127.0.0.1:58412` (`DB_PORT`) — loopback uniquement, pour l'administration locale | +| `db` | PostgreSQL 16 (vierge : l'application l'initialise elle-même) | `127.0.0.1:58412` (`DB_PORT`) — loopback uniquement, pour l'administration locale | ``` Navigateur ──> app (Express :8321) ──> db (PostgreSQL, 127.0.0.1:58412) @@ -44,8 +44,9 @@ réseau du proxy. (`SLUC Business Club.dc.html`). - **Backend** : Express, `pg` (requêtes paramétrées), `zod` (validation), `bcryptjs` (hachage), JWT en cookie httpOnly, `helmet`, `express-rate-limit`, `multer` (uploads). -- **Base** : PostgreSQL 16, initialisée par `db/init/` (rôle applicatif restreint, - schéma, données de démonstration). +- **Base** : PostgreSQL 16. L'application l'initialise **elle-même à chaque + démarrage**, de façon idempotente (base, rôle restreint `sbc_app`, schéma, + données de démo) : aucun script monté, aucun état de volume requis. ## Démarrage @@ -137,11 +138,11 @@ formulaire « Mot de passe », endpoint `POST /api/auth/change-password`). ## Dépannage -- **`password authentication failed for user "sbc_app"`** : le volume de la base - a été initialisé avec un autre `APP_DB_PASSWORD`. Le service one-shot `db-sync` - resynchronise automatiquement le mot de passe du rôle à chaque démarrage de la - stack — redéployez simplement. Si le mot de passe **superuser** a lui aussi - changé (`POSTGRES_PASSWORD`), remettez l'ancienne valeur ou supprimez le volume +- **`password authentication failed for user "sbc_app"`** : impossible dans la + version courante tant que `PG_SUPERUSER_PASSWORD` correspond au mot de passe + superuser du volume — l'application recrée/realigne le rôle et le schéma à + chaque démarrage. Si le mot de passe **superuser** du volume diffère de + `POSTGRES_PASSWORD`, remettez l'ancienne valeur ou supprimez le volume `db_data` pour repartir de zéro (les données de démo seront recréées). - **Port déjà utilisé** : changez `APP_PORT` ou `DB_PORT` dans les variables d'environnement. @@ -170,7 +171,6 @@ cd web && npm install && npm run dev # proxy /api → localhost:8321 ├── docker-compose.yml ├── Dockerfile # multi-étages : build React → dépendances API → image finale ├── .env.example -├── db/init/ # 01 rôle applicatif · 02 schéma · 03 données de démo -├── server/ # API Express (src/routes, src/middleware, uploads, statique) +├── server/ # API Express (src/routes, src/sql = schéma + seed, uploads) └── web/ # sources React + Vite (compilées dans l'image) ``` diff --git a/db/init/01-app-role.sh b/db/init/01-app-role.sh deleted file mode 100644 index 1d58766..0000000 --- a/db/init/01-app-role.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Creates the restricted application role the API connects with. -# Runs once, on first initialization of the data volume. -set -euo pipefail - -# Escape single quotes so an exotic password cannot break the SQL literal -ESCAPED_PASSWORD="${APP_DB_PASSWORD//\'/\'\'}" - -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - CREATE ROLE sbc_app LOGIN PASSWORD '${ESCAPED_PASSWORD}' NOSUPERUSER NOCREATEDB NOCREATEROLE; -EOSQL diff --git a/db/init/03-seed.sql b/db/init/03-seed.sql deleted file mode 100644 index a2b577a..0000000 --- a/db/init/03-seed.sql +++ /dev/null @@ -1,68 +0,0 @@ --- SLUC Business Club — demo seed data (content from the design mock) --- User passwords are NOT stored here: accounts are created with the --- unusable placeholder hash '*seed*' and receive their real (bcrypt) --- password from the API at startup, based on ADMIN_INITIAL_PASSWORD / --- MEMBER_INITIAL_PASSWORD environment variables. - -INSERT INTO categories (name) VALUES - ('Conseil & Finance'), - ('Industrie & BTP'), - ('Communication & Digital'), - ('Services'), - ('Santé'); - -INSERT INTO members (nom, secteur, categorie_id, dirigeant, adhesion, email, tel, site, presentation, valide) VALUES - ('Lorraine Assurances', 'Assurance & Prévoyance', 1, 'Sophie Marchand', 2014, 'contact@lorraine-assurances.fr', '+33 3 83 12 45 60', 'lorraine-assurances.fr', 'Cabinet de courtage indépendant accompagnant les entreprises et dirigeants du Grand Est sur l''ensemble de leurs besoins en assurance et prévoyance.', true), - ('Groupe Batigest', 'BTP & Construction', 2, 'Philippe Aubry', 2009, 'contact@batigest.fr', '+33 3 83 22 18 04', 'groupe-batigest.fr', 'Entreprise générale de construction et de rénovation, acteur majeur du bâtiment en Lorraine depuis plus de trente ans.', true), - ('Moselle Digital', 'Agence digitale', 3, 'Karim Benali', 2019, 'hello@moselle-digital.fr', '+33 3 83 55 71 22', 'moselle-digital.fr', 'Agence de communication digitale spécialisée dans la création de sites, le référencement et les stratégies social media pour les PME.', true), - ('Renard & Associés', 'Cabinet d''avocats', 1, 'Me Claire Renard', 2012, 'contact@renard-avocats.fr', '+33 3 83 30 09 15', 'renard-avocats.fr', 'Cabinet d''avocats d''affaires conseillant dirigeants et entreprises en droit des sociétés, droit social et droit commercial.', true), - ('NancyTech Solutions', 'Solutions informatiques', 3, 'Thomas Villeret', 2020, 'contact@nancytech.fr', '+33 3 83 41 88 90', 'nancytech.fr', 'Infogérance, cybersécurité et déploiement d''outils métiers pour accompagner la transformation numérique des entreprises régionales.', true), - ('Est Immobilier', 'Immobilier d''entreprise', 4, 'Nathalie Perrin', 2016, 'contact@est-immobilier.fr', '+33 3 83 17 62 30', 'est-immobilier.fr', 'Conseil en immobilier d''entreprise : bureaux, locaux commerciaux et investissement sur l''agglomération nancéienne.', true), - ('Vosges Logistique', 'Transport & Logistique', 4, 'Bruno Kieffer', 2011, 'contact@vosges-logistique.fr', '+33 3 29 34 12 78', 'vosges-logistique.fr', 'Solutions de transport, entreposage et distribution au service des industriels et distributeurs du Grand Est.', false), - ('Meurthe Industries', 'Industrie & Métallurgie', 2, 'Laurent Schmitt', 2008, 'contact@meurthe-industries.fr', '+33 3 83 49 05 11', 'meurthe-industries.fr', 'Sous-traitance industrielle de précision et travail des métaux pour les secteurs de l''automobile et de l''énergie.', true), - ('Grand Est Finance', 'Conseil financier', 1, 'Isabelle Fontaine', 2017, 'contact@grandest-finance.fr', '+33 3 83 25 44 90', 'grandest-finance.fr', 'Conseil en gestion de patrimoine et financement d''entreprise, aux côtés des dirigeants dans leurs projets de croissance.', true), - ('Atelier Lumière', 'Communication & Design', 3, 'Julie Mercier', 2021, 'bonjour@atelier-lumiere.fr', '+33 3 83 60 77 08', 'atelier-lumiere.fr', 'Studio de création graphique et d''identité de marque, du logo à la signalétique, pour donner du sens à votre image.', true), - ('Pharma Lorraine', 'Laboratoire pharmaceutique', 5, 'Dr Antoine Rousseau', 2013, 'contact@pharma-lorraine.fr', '+33 3 83 90 21 47', 'pharma-lorraine.fr', 'Laboratoire de production et de distribution de solutions de santé, engagé dans l''innovation et la qualité.', false), - ('Cristal Événements', 'Événementiel', 4, 'Émilie Colin', 2018, 'contact@cristal-events.fr', '+33 3 83 38 66 12', 'cristal-events.fr', 'Agence événementielle imaginant séminaires, soirées d''entreprise et événements sur-mesure clés en main.', true); - -INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places) VALUES - ('Afterwork Business & Basket', '2026-09-17', '18h30', 'Palais des Sports J. Weille', 'Networking en tribune VIP autour d''un cocktail, suivi du match SLUC Nancy – Cholet. Le rendez-vous incontournable de la rentrée.', 40), - ('Petit-déjeuner des Dirigeants', '2026-10-06', '08h00', 'Hôtel Mercure Nancy Centre', 'Échanges entre dirigeants autour d''un intervenant invité, dans un format intimiste propice aux affaires.', 25), - ('Conférence — L''esprit d''équipe', '2026-11-19', '19h00', 'Grand Salon, Hôtel de Ville', 'Un coach de haut niveau partage les ressorts de la performance collective, du terrain à l''entreprise.', 80); - -INSERT INTO inscriptions (rencontre_id, nom, entreprise, email, tel, statut) VALUES - (1, 'Sophie Marchand', 'Lorraine Assurances', 's.marchand@lorraine-assurances.fr', '+33 3 83 12 45 60', 'confirmee'), - (1, 'Karim Benali', 'Moselle Digital', 'k.benali@moselle-digital.fr', '+33 3 83 55 71 22', 'en_attente'), - (2, 'Thomas Villeret', 'NancyTech Solutions', 't.villeret@nancytech.fr', '+33 3 83 41 88 90', 'confirmee'), - (3, 'Nathalie Perrin', 'Est Immobilier', 'n.perrin@est-immobilier.fr', '+33 3 83 17 62 30', 'en_attente'), - (2, 'Bruno Kieffer', 'Vosges Logistique', 'b.kieffer@vosges-logistique.fr', '+33 3 29 34 12 78', 'confirmee'), - (1, 'Julie Mercier', 'Atelier Lumière', 'j.mercier@atelier-lumiere.fr', '+33 3 83 60 77 08', 'en_attente'), - (1, 'Laurent Schmitt', 'Meurthe Industries', 'l.schmitt@meurthe-industries.fr', '+33 3 83 49 05 11', 'confirmee'), - (3, 'Isabelle Fontaine', 'Grand Est Finance', 'i.fontaine@grandest-finance.fr', '+33 3 83 25 44 90', 'confirmee'), - (2, 'Me Claire Renard', 'Renard & Associés', 'c.renard@renard-avocats.fr', '+33 3 83 30 09 15', 'en_attente'), - (1, 'Émilie Colin', 'Cristal Événements', 'e.colin@cristal-events.fr', '+33 3 83 38 66 12', 'confirmee'), - (3, 'Philippe Aubry', 'Groupe Batigest', 'p.aubry@batigest.fr', '+33 3 83 22 18 04', 'confirmee'); - -INSERT INTO rencontres_passees (date_label, lieu, titre, texte, participants, nb_photos) VALUES - ('Juin 2026', 'Château de Rémicourt', 'Soirée de Gala annuelle', 'Plus de 200 convives réunis pour célébrer une saison d''exception. Une soirée d''élégance mêlant remise de trophées, dîner gastronomique et rencontres privilégiées entre membres du Club.', 210, 48), - ('Avril 2026', 'Site industriel, Florange', 'Visite privée — Usine ArcelorMittal', 'Immersion industrielle exclusive pour nos membres au cœur d''un fleuron de la métallurgie lorraine, suivie d''un échange avec la direction du site.', 34, 22), - ('Février 2026', 'La Filature, Nancy', 'Table ronde — Digitaliser sa PME', 'Retours d''expérience et bonnes pratiques : trois dirigeants membres ont partagé leur parcours de transformation numérique devant une salle comble.', 56, 15); - -INSERT INTO demandes_adhesion (nom, fonction, entreprise, email) VALUES - ('Marc Dubois', 'Directeur général', 'Dubois Traiteur', 'm.dubois@dubois-traiteur.fr'), - ('Léa Hoffmann', 'Présidente', 'Hoffmann Conseil', 'l.hoffmann@hoffmann-conseil.fr'), - ('Julien Weber', 'Gérant', 'Weber Menuiserie', 'j.weber@weber-menuiserie.fr'); - -INSERT INTO site_content (key, value) VALUES - ('hero_quote_text', 'On ne réussit jamais seul.'), - ('hero_quote_author', 'L''esprit du Club'), - ('hero_photo', ''); - --- Accounts: one admin + one portal account per member company. --- '*seed*' is an intentionally unusable hash; real passwords are applied --- by the API at startup from environment variables. -INSERT INTO users (email, password_hash, role, member_id) -SELECT email, '*seed*', 'member', id FROM members WHERE email IS NOT NULL; - -INSERT INTO users (email, password_hash, role) VALUES - ('admin@sluc-businessclub.fr', '*seed*', 'admin'); diff --git a/db/sync-app-role.sh b/db/sync-app-role.sh deleted file mode 100644 index 7213a71..0000000 --- a/db/sync-app-role.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# Runs at every stack start (db-sync service): keeps the sbc_app role's -# password in sync with APP_DB_PASSWORD, even when the database volume was -# initialized with a different value. Idempotent. -set -eu - -ESCAPED=$(printf %s "$APP_DB_PASSWORD" | sed "s/'/''/g") - -psql -v ON_ERROR_STOP=1 < fs.readFileSync(new URL(`./sql/${name}`, import.meta.url), 'utf8'); + +const escLiteral = (v) => String(v).replace(/'/g, "''"); +const escIdent = (v) => `"${String(v).replace(/"/g, '""')}"`; + +async function connectWithRetry(options, retries = 30, delayMs = 1000) { + for (let i = 1; i <= retries; i++) { + const client = new pg.Client({ ...options, connectionTimeoutMillis: 5000 }); + try { + await client.connect(); + return client; + } catch (err) { + await client.end().catch(() => {}); + if (i === retries) throw err; + await new Promise((r) => setTimeout(r, delayMs)); + } + } +} + +// Fully idempotent bootstrap, run at every startup with the PostgreSQL +// superuser: heals ANY volume state (missing database, missing role, wrong +// role password, missing schema, empty data). No init scripts, no bind +// mounts, no extra container required. +export async function bootstrapDatabase() { + if (!config.db.superuserPassword) { + console.log('PG_SUPERUSER_PASSWORD not set: skipping automatic database bootstrap.'); + return; + } + const su = { + host: config.db.host, + port: config.db.port, + user: config.db.superuser, + password: config.db.superuserPassword, + }; + + // 1) Maintenance database: ensure application database and role exist + const admin = await connectWithRetry({ ...su, database: 'postgres' }); + try { + const dbExists = await admin.query('SELECT 1 FROM pg_database WHERE datname = $1', [ + config.db.database, + ]); + if (dbExists.rowCount === 0) { + await admin.query(`CREATE DATABASE ${escIdent(config.db.database)}`); + console.log(`Database "${config.db.database}" created.`); + } + const roleExists = await admin.query('SELECT 1 FROM pg_roles WHERE rolname = $1', [ + config.db.user, + ]); + if (roleExists.rowCount === 0) { + await admin.query( + `CREATE ROLE ${escIdent(config.db.user)} LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE` + ); + console.log(`Role "${config.db.user}" created.`); + } + // Always realign the password with the current environment value + await admin.query( + `ALTER ROLE ${escIdent(config.db.user)} WITH LOGIN PASSWORD '${escLiteral(config.db.password)}' NOSUPERUSER NOCREATEDB NOCREATEROLE` + ); + } finally { + await admin.end().catch(() => {}); + } + + // 2) Application database: schema, demo seed (once), grants + const db = await connectWithRetry({ ...su, database: config.db.database }); + try { + await db.query(read('schema.sql')); + const seeded = await db.query('SELECT COUNT(*)::int AS n FROM categories'); + if (seeded.rows[0].n === 0) { + await db.query(read('seed.sql')); + console.log('Demo data seeded.'); + } + await db.query(` + GRANT USAGE ON SCHEMA public TO ${escIdent(config.db.user)}; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${escIdent(config.db.user)}; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${escIdent(config.db.user)}; + `); + } finally { + await db.end().catch(() => {}); + } + console.log('Database bootstrap complete.'); +} diff --git a/server/src/config.js b/server/src/config.js index 69fdc15..5a8d973 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -29,6 +29,10 @@ export const config = { database: process.env.PGDATABASE || 'sbc', user: process.env.PGUSER || 'sbc_app', password: required('PGPASSWORD'), + // When provided, the app bootstraps the database itself at startup + // (create database/role, apply schema, seed demo data) — idempotent. + superuser: process.env.PG_SUPERUSER || 'postgres', + superuserPassword: process.env.PG_SUPERUSER_PASSWORD || '', }, jwtSecret, cookieSecure: process.env.COOKIE_SECURE === 'true', diff --git a/server/src/index.js b/server/src/index.js index e1fd47d..047a1df 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -6,6 +6,7 @@ import compression from 'compression'; import cookieParser from 'cookie-parser'; import { config } from './config.js'; import { waitForDb } from './db.js'; +import { bootstrapDatabase } from './bootstrap-db.js'; import { applyInitialPasswords } from './bootstrap.js'; import { attachUser } from './middleware/auth.js'; import { csrfOriginCheck, globalLimiter } from './middleware/security.js'; @@ -107,6 +108,7 @@ app.use((err, _req, res, _next) => { }); try { + await bootstrapDatabase(); await waitForDb(); await applyInitialPasswords(); app.listen(config.port, () => { diff --git a/db/init/02-schema.sql b/server/src/sql/schema.sql similarity index 84% rename from db/init/02-schema.sql rename to server/src/sql/schema.sql index 1a378fb..bcf5b6b 100644 --- a/db/init/02-schema.sql +++ b/server/src/sql/schema.sql @@ -1,12 +1,12 @@ --- SLUC Business Club — schema +-- SLUC Business Club — idempotent schema (applied at every app startup) CREATE EXTENSION IF NOT EXISTS citext; -CREATE TABLE categories ( +CREATE TABLE IF NOT EXISTS categories ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE CHECK (char_length(name) BETWEEN 1 AND 80) ); -CREATE TABLE members ( +CREATE TABLE IF NOT EXISTS members ( id SERIAL PRIMARY KEY, nom TEXT NOT NULL CHECK (char_length(nom) BETWEEN 1 AND 120), secteur TEXT NOT NULL DEFAULT '' CHECK (char_length(secteur) <= 120), @@ -24,7 +24,7 @@ CREATE TABLE members ( updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE TABLE users ( +CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email CITEXT NOT NULL UNIQUE CHECK (char_length(email) <= 254), password_hash TEXT NOT NULL, @@ -33,7 +33,7 @@ CREATE TABLE users ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE TABLE rencontres ( +CREATE TABLE IF NOT EXISTS rencontres ( id SERIAL PRIMARY KEY, titre TEXT NOT NULL CHECK (char_length(titre) BETWEEN 1 AND 200), date_renc DATE NOT NULL, @@ -44,7 +44,7 @@ CREATE TABLE rencontres ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE TABLE inscriptions ( +CREATE TABLE IF NOT EXISTS inscriptions ( id SERIAL PRIMARY KEY, rencontre_id INTEGER NOT NULL REFERENCES rencontres(id) ON DELETE CASCADE, nom TEXT NOT NULL CHECK (char_length(nom) BETWEEN 1 AND 120), @@ -54,9 +54,9 @@ CREATE TABLE inscriptions ( statut TEXT NOT NULL DEFAULT 'en_attente' CHECK (statut IN ('confirmee', 'en_attente')), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE INDEX idx_inscriptions_rencontre ON inscriptions(rencontre_id); +CREATE INDEX IF NOT EXISTS idx_inscriptions_rencontre ON inscriptions(rencontre_id); -CREATE TABLE rencontres_passees ( +CREATE TABLE IF NOT EXISTS rencontres_passees ( id SERIAL PRIMARY KEY, date_label TEXT NOT NULL, lieu TEXT NOT NULL, @@ -66,7 +66,7 @@ CREATE TABLE rencontres_passees ( nb_photos INTEGER NOT NULL DEFAULT 0 ); -CREATE TABLE demandes_adhesion ( +CREATE TABLE IF NOT EXISTS demandes_adhesion ( id SERIAL PRIMARY KEY, nom TEXT NOT NULL CHECK (char_length(nom) BETWEEN 1 AND 120), fonction TEXT NOT NULL DEFAULT '' CHECK (char_length(fonction) <= 120), @@ -76,12 +76,7 @@ CREATE TABLE demandes_adhesion ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE TABLE site_content ( +CREATE TABLE IF NOT EXISTS site_content ( key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '' ); - --- Least-privilege grants for the application role (no DDL, no other schemas) -GRANT USAGE ON SCHEMA public TO sbc_app; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO sbc_app; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO sbc_app; diff --git a/server/src/sql/seed.sql b/server/src/sql/seed.sql new file mode 100644 index 0000000..579dda6 --- /dev/null +++ b/server/src/sql/seed.sql @@ -0,0 +1,67 @@ +-- Demo data — executed only when the categories table is empty. +-- All references use name lookups so the seed works whatever the sequences. + +INSERT INTO categories (name) VALUES + ('Conseil & Finance'), + ('Industrie & BTP'), + ('Communication & Digital'), + ('Services'), + ('Santé'); + +INSERT INTO members (nom, secteur, categorie_id, dirigeant, adhesion, email, tel, site, presentation, valide) VALUES + ('Lorraine Assurances', 'Assurance & Prévoyance', (SELECT id FROM categories WHERE name = 'Conseil & Finance'), 'Sophie Marchand', 2014, 'contact@lorraine-assurances.fr', '+33 3 83 12 45 60', 'lorraine-assurances.fr', 'Cabinet de courtage indépendant accompagnant les entreprises et dirigeants du Grand Est sur l''ensemble de leurs besoins en assurance et prévoyance.', true), + ('Groupe Batigest', 'BTP & Construction', (SELECT id FROM categories WHERE name = 'Industrie & BTP'), 'Philippe Aubry', 2009, 'contact@batigest.fr', '+33 3 83 22 18 04', 'groupe-batigest.fr', 'Entreprise générale de construction et de rénovation, acteur majeur du bâtiment en Lorraine depuis plus de trente ans.', true), + ('Moselle Digital', 'Agence digitale', (SELECT id FROM categories WHERE name = 'Communication & Digital'), 'Karim Benali', 2019, 'hello@moselle-digital.fr', '+33 3 83 55 71 22', 'moselle-digital.fr', 'Agence de communication digitale spécialisée dans la création de sites, le référencement et les stratégies social media pour les PME.', true), + ('Renard & Associés', 'Cabinet d''avocats', (SELECT id FROM categories WHERE name = 'Conseil & Finance'), 'Me Claire Renard', 2012, 'contact@renard-avocats.fr', '+33 3 83 30 09 15', 'renard-avocats.fr', 'Cabinet d''avocats d''affaires conseillant dirigeants et entreprises en droit des sociétés, droit social et droit commercial.', true), + ('NancyTech Solutions', 'Solutions informatiques', (SELECT id FROM categories WHERE name = 'Communication & Digital'), 'Thomas Villeret', 2020, 'contact@nancytech.fr', '+33 3 83 41 88 90', 'nancytech.fr', 'Infogérance, cybersécurité et déploiement d''outils métiers pour accompagner la transformation numérique des entreprises régionales.', true), + ('Est Immobilier', 'Immobilier d''entreprise', (SELECT id FROM categories WHERE name = 'Services'), 'Nathalie Perrin', 2016, 'contact@est-immobilier.fr', '+33 3 83 17 62 30', 'est-immobilier.fr', 'Conseil en immobilier d''entreprise : bureaux, locaux commerciaux et investissement sur l''agglomération nancéienne.', true), + ('Vosges Logistique', 'Transport & Logistique', (SELECT id FROM categories WHERE name = 'Services'), 'Bruno Kieffer', 2011, 'contact@vosges-logistique.fr', '+33 3 29 34 12 78', 'vosges-logistique.fr', 'Solutions de transport, entreposage et distribution au service des industriels et distributeurs du Grand Est.', false), + ('Meurthe Industries', 'Industrie & Métallurgie', (SELECT id FROM categories WHERE name = 'Industrie & BTP'), 'Laurent Schmitt', 2008, 'contact@meurthe-industries.fr', '+33 3 83 49 05 11', 'meurthe-industries.fr', 'Sous-traitance industrielle de précision et travail des métaux pour les secteurs de l''automobile et de l''énergie.', true), + ('Grand Est Finance', 'Conseil financier', (SELECT id FROM categories WHERE name = 'Conseil & Finance'), 'Isabelle Fontaine', 2017, 'contact@grandest-finance.fr', '+33 3 83 25 44 90', 'grandest-finance.fr', 'Conseil en gestion de patrimoine et financement d''entreprise, aux côtés des dirigeants dans leurs projets de croissance.', true), + ('Atelier Lumière', 'Communication & Design', (SELECT id FROM categories WHERE name = 'Communication & Digital'), 'Julie Mercier', 2021, 'bonjour@atelier-lumiere.fr', '+33 3 83 60 77 08', 'atelier-lumiere.fr', 'Studio de création graphique et d''identité de marque, du logo à la signalétique, pour donner du sens à votre image.', true), + ('Pharma Lorraine', 'Laboratoire pharmaceutique', (SELECT id FROM categories WHERE name = 'Santé'), 'Dr Antoine Rousseau', 2013, 'contact@pharma-lorraine.fr', '+33 3 83 90 21 47', 'pharma-lorraine.fr', 'Laboratoire de production et de distribution de solutions de santé, engagé dans l''innovation et la qualité.', false), + ('Cristal Événements', 'Événementiel', (SELECT id FROM categories WHERE name = 'Services'), 'Émilie Colin', 2018, 'contact@cristal-events.fr', '+33 3 83 38 66 12', 'cristal-events.fr', 'Agence événementielle imaginant séminaires, soirées d''entreprise et événements sur-mesure clés en main.', true); + +INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places) VALUES + ('Afterwork Business & Basket', '2026-09-17', '18h30', 'Palais des Sports J. Weille', 'Networking en tribune VIP autour d''un cocktail, suivi du match SLUC Nancy – Cholet. Le rendez-vous incontournable de la rentrée.', 40), + ('Petit-déjeuner des Dirigeants', '2026-10-06', '08h00', 'Hôtel Mercure Nancy Centre', 'Échanges entre dirigeants autour d''un intervenant invité, dans un format intimiste propice aux affaires.', 25), + ('Conférence — L''esprit d''équipe', '2026-11-19', '19h00', 'Grand Salon, Hôtel de Ville', 'Un coach de haut niveau partage les ressorts de la performance collective, du terrain à l''entreprise.', 80); + +INSERT INTO inscriptions (rencontre_id, nom, entreprise, email, tel, statut) VALUES + ((SELECT id FROM rencontres WHERE titre = 'Afterwork Business & Basket'), 'Sophie Marchand', 'Lorraine Assurances', 's.marchand@lorraine-assurances.fr', '+33 3 83 12 45 60', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Afterwork Business & Basket'), 'Karim Benali', 'Moselle Digital', 'k.benali@moselle-digital.fr', '+33 3 83 55 71 22', 'en_attente'), + ((SELECT id FROM rencontres WHERE titre = 'Petit-déjeuner des Dirigeants'), 'Thomas Villeret', 'NancyTech Solutions', 't.villeret@nancytech.fr', '+33 3 83 41 88 90', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Conférence — L''esprit d''équipe'), 'Nathalie Perrin', 'Est Immobilier', 'n.perrin@est-immobilier.fr', '+33 3 83 17 62 30', 'en_attente'), + ((SELECT id FROM rencontres WHERE titre = 'Petit-déjeuner des Dirigeants'), 'Bruno Kieffer', 'Vosges Logistique', 'b.kieffer@vosges-logistique.fr', '+33 3 29 34 12 78', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Afterwork Business & Basket'), 'Julie Mercier', 'Atelier Lumière', 'j.mercier@atelier-lumiere.fr', '+33 3 83 60 77 08', 'en_attente'), + ((SELECT id FROM rencontres WHERE titre = 'Afterwork Business & Basket'), 'Laurent Schmitt', 'Meurthe Industries', 'l.schmitt@meurthe-industries.fr', '+33 3 83 49 05 11', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Conférence — L''esprit d''équipe'), 'Isabelle Fontaine', 'Grand Est Finance', 'i.fontaine@grandest-finance.fr', '+33 3 83 25 44 90', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Petit-déjeuner des Dirigeants'), 'Me Claire Renard', 'Renard & Associés', 'c.renard@renard-avocats.fr', '+33 3 83 30 09 15', 'en_attente'), + ((SELECT id FROM rencontres WHERE titre = 'Afterwork Business & Basket'), 'Émilie Colin', 'Cristal Événements', 'e.colin@cristal-events.fr', '+33 3 83 38 66 12', 'confirmee'), + ((SELECT id FROM rencontres WHERE titre = 'Conférence — L''esprit d''équipe'), 'Philippe Aubry', 'Groupe Batigest', 'p.aubry@batigest.fr', '+33 3 83 22 18 04', 'confirmee'); + +INSERT INTO rencontres_passees (date_label, lieu, titre, texte, participants, nb_photos) VALUES + ('Juin 2026', 'Château de Rémicourt', 'Soirée de Gala annuelle', 'Plus de 200 convives réunis pour célébrer une saison d''exception. Une soirée d''élégance mêlant remise de trophées, dîner gastronomique et rencontres privilégiées entre membres du Club.', 210, 48), + ('Avril 2026', 'Site industriel, Florange', 'Visite privée — Usine ArcelorMittal', 'Immersion industrielle exclusive pour nos membres au cœur d''un fleuron de la métallurgie lorraine, suivie d''un échange avec la direction du site.', 34, 22), + ('Février 2026', 'La Filature, Nancy', 'Table ronde — Digitaliser sa PME', 'Retours d''expérience et bonnes pratiques : trois dirigeants membres ont partagé leur parcours de transformation numérique devant une salle comble.', 56, 15); + +INSERT INTO demandes_adhesion (nom, fonction, entreprise, email) VALUES + ('Marc Dubois', 'Directeur général', 'Dubois Traiteur', 'm.dubois@dubois-traiteur.fr'), + ('Léa Hoffmann', 'Présidente', 'Hoffmann Conseil', 'l.hoffmann@hoffmann-conseil.fr'), + ('Julien Weber', 'Gérant', 'Weber Menuiserie', 'j.weber@weber-menuiserie.fr'); + +INSERT INTO site_content (key, value) VALUES + ('hero_quote_text', 'On ne réussit jamais seul.'), + ('hero_quote_author', 'L''esprit du Club'), + ('hero_photo', '') +ON CONFLICT (key) DO NOTHING; + +-- Accounts: created locked ('*seed*'); real passwords are applied at startup +-- from ADMIN_INITIAL_PASSWORD / MEMBER_INITIAL_PASSWORD. +INSERT INTO users (email, password_hash, role, member_id) +SELECT email, '*seed*', 'member', id FROM members WHERE email IS NOT NULL +ON CONFLICT (email) DO NOTHING; + +INSERT INTO users (email, password_hash, role) VALUES + ('admin@sluc-businessclub.fr', '*seed*', 'admin') +ON CONFLICT (email) DO NOTHING;