mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-24 07:38:04 +02:00
Base vide par défaut : données de démonstration isolées et purgeables
Le seed ne crée plus que le référentiel des catégories et le compte administrateur initial. Les adhérents, promotions, demandes d'adhésion, entrées du journal et comptes de démonstration sont déplacés dans src/db/demo-data.ts et ne sont insérés qu'avec SEED_DEMO=true. Nouveau script `npm run db:purge-demo` (bundlé en dist/purge-demo.cjs pour le conteneur) : retire exactement ce jeu de démonstration d'une base qui l'a reçu lors des démarrages précédents, sans toucher aux saisies de l'association ni aux catégories. Idempotent, en transaction ; un compte réel rattaché à une fiche de démo est détaché et non supprimé. Vérifié en local sur Postgres 16 : seed par défaut = 59 catégories + 1 admin et rien d'autre ; seed avec SEED_DEMO=true = jeu complet ; purge = retour à l'état vide ; seconde purge sans effet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RAQsCp4nnZbwexCg7NDBHE
This commit is contained in:
7 files changed
+313
-149
No files matched your search
+6
-2
@@ -27,8 +27,12 @@ SEED_ADMIN_EMAIL=admin@plein-r.fr
|
|||||||
# affiché UNE FOIS dans les journaux, et doit être changé à la première connexion.
|
# affiché UNE FOIS dans les journaux, et doit être changé à la première connexion.
|
||||||
SEED_ADMIN_PASSWORD=
|
SEED_ADMIN_PASSWORD=
|
||||||
SEED_ADMIN_NAME=Administrateur Plein R
|
SEED_ADMIN_NAME=Administrateur Plein R
|
||||||
# Comptes et contenus de démonstration (mots de passe connus de tous) :
|
# Données de démonstration (adhérents, promotions, demandes, journal et
|
||||||
# uniquement pour un poste de développement, jamais en production.
|
# comptes au mot de passe connu de tous) : uniquement sur un poste de
|
||||||
|
# développement, jamais en production. Par défaut la base reste vide, avec le
|
||||||
|
# référentiel des catégories et le compte administrateur seulement.
|
||||||
|
# Pour retirer des données de démo déjà en base : npm run db:purge-demo
|
||||||
|
# (ou, dans le conteneur : node dist/purge-demo.cjs).
|
||||||
SEED_DEMO=false
|
SEED_DEMO=false
|
||||||
|
|
||||||
# ---- App ----
|
# ---- App ----
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ npm run dev # dev server
|
|||||||
npm run build # production build
|
npm run build # production build
|
||||||
npm run db:generate # regenerate SQL after editing src/db/schema.ts
|
npm run db:generate # regenerate SQL after editing src/db/schema.ts
|
||||||
npm run db:migrate # apply migrations
|
npm run db:migrate # apply migrations
|
||||||
npm run db:seed # seed demo data
|
npm run db:seed # catégories + admin initial ; données de démo si SEED_DEMO=true
|
||||||
|
npm run db:purge-demo # retire les données de démo d'une base qui les a reçues
|
||||||
docker compose up --build # full stack
|
docker compose up --build # full stack
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -101,10 +102,13 @@ site sans traitement supplémentaire.
|
|||||||
invitation staff) ne sont **jamais stockés** : l'action les renvoie et le
|
invitation staff) ne sont **jamais stockés** : l'action les renvoie et le
|
||||||
composant `OneTimeCredentials` les affiche une seule fois, sans redirection.
|
composant `OneTimeCredentials` les affiche une seule fois, sans redirection.
|
||||||
`users.must_change_password` seul persiste.
|
`users.must_change_password` seul persiste.
|
||||||
- Le seed ne crée en production que l'administrateur initial, avec un mot de
|
- Le seed ne crée en production que le référentiel des catégories et
|
||||||
passe aléatoire affiché une fois dans les journaux (ou `SEED_ADMIN_PASSWORD`)
|
l'administrateur initial, avec un mot de passe aléatoire affiché une fois
|
||||||
et un changement obligatoire à la première connexion. Les comptes de démo
|
dans les journaux (ou `SEED_ADMIN_PASSWORD`) et un changement obligatoire à
|
||||||
(`changeme123`) exigent `SEED_DEMO=true`.
|
la première connexion. Toutes les données de démonstration (adhérents,
|
||||||
|
promotions, demandes, journal, comptes `changeme123`) vivent dans
|
||||||
|
`src/db/demo-data.ts`, exigent `SEED_DEMO=true`, et `npm run db:purge-demo`
|
||||||
|
les retire d'une base existante.
|
||||||
- Sessions JWT limitées à 7 jours (`auth.config.ts`), HSTS et suppression de
|
- Sessions JWT limitées à 7 jours (`auth.config.ts`), HSTS et suppression de
|
||||||
`X-Powered-By` dans `next.config.mjs`. Le port Postgres de `docker-compose`
|
`X-Powered-By` dans `next.config.mjs`. Le port Postgres de `docker-compose`
|
||||||
n'est publié que sur `127.0.0.1`.
|
n'est publié que sur `127.0.0.1`.
|
||||||
|
|||||||
+2
-1
@@ -65,7 +65,8 @@ services:
|
|||||||
# Vide = un mot de passe aléatoire est généré au premier démarrage et
|
# Vide = un mot de passe aléatoire est généré au premier démarrage et
|
||||||
# affiché une seule fois dans les journaux du conteneur.
|
# affiché une seule fois dans les journaux du conteneur.
|
||||||
SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD:-}
|
SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD:-}
|
||||||
# Comptes de démonstration (mot de passe connu) : jamais en production.
|
# Données de démonstration (adhérents, promotions, comptes au mot de
|
||||||
|
# passe connu) : jamais en production. Base vide par défaut.
|
||||||
SEED_DEMO: ${SEED_DEMO:-false}
|
SEED_DEMO: ${SEED_DEMO:-false}
|
||||||
SEED_ADMIN_NAME: ${SEED_ADMIN_NAME:-Administrateur Plein R}
|
SEED_ADMIN_NAME: ${SEED_ADMIN_NAME:-Administrateur Plein R}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "tsx src/db/migrate.ts",
|
"db:migrate": "tsx src/db/migrate.ts",
|
||||||
"db:seed": "tsx src/db/seed.ts",
|
"db:seed": "tsx src/db/seed.ts",
|
||||||
"build:scripts": "esbuild src/db/migrate.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/migrate.cjs --external:pg-native && esbuild src/db/seed.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/seed.cjs --external:pg-native",
|
"db:purge-demo": "tsx src/db/purge-demo.ts",
|
||||||
|
"build:scripts": "esbuild src/db/migrate.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/migrate.cjs --external:pg-native && esbuild src/db/seed.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/seed.cjs --external:pg-native && esbuild src/db/purge-demo.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/purge-demo.cjs --external:pg-native",
|
||||||
"test": "tsx --test tests/**/*.test.ts"
|
"test": "tsx --test tests/**/*.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Données de démonstration.
|
||||||
|
*
|
||||||
|
* Elles ne sont insérées que par `npm run db:seed` avec `SEED_DEMO=true`, et
|
||||||
|
* `npm run db:purge-demo` sait les retirer d'une base qui les a reçues. Tout
|
||||||
|
* ce qui identifie ces enregistrements (e-mails, titres, noms, messages) vit
|
||||||
|
* ici pour que les deux scripts restent alignés.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEMO_PASSWORD = "changeme123";
|
||||||
|
|
||||||
|
export function demoMembers(catId: (slug: string) => number | null) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Au Bon Pain",
|
||||||
|
email: "contact@aubonpain.fr",
|
||||||
|
categoryId: catId("boulangerie"),
|
||||||
|
city: "Frouard",
|
||||||
|
address: "12 rue de la République",
|
||||||
|
postalCode: "54390",
|
||||||
|
phone: "03 83 49 00 00",
|
||||||
|
website: "https://www.aubonpain-frouard.fr",
|
||||||
|
memberSince: 2021,
|
||||||
|
tags: "Levain naturel, Produits locaux, Fait maison, Sans conservateur",
|
||||||
|
hours: "Mardi – Vendredi|7h – 19h30\nSamedi|7h – 19h\nDimanche|7h – 13h\nLundi|Fermé",
|
||||||
|
description:
|
||||||
|
"Boulangerie artisanale familiale installée au cœur de Frouard depuis trois générations. Nous travaillons chaque jour des farines locales et un levain naturel pour vous proposer pains, viennoiseries et pâtisseries faits maison.\nNotre équipe vous accueille du mardi au dimanche matin. Spécialités : pain au levain, baguette de tradition, kouglof et tartes aux fruits de saison.",
|
||||||
|
status: "active" as const,
|
||||||
|
highlighted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Café des Arts",
|
||||||
|
email: "hello@cafedesarts.fr",
|
||||||
|
categoryId: catId("restauration"),
|
||||||
|
city: "Pompey",
|
||||||
|
address: "3 place Stanislas",
|
||||||
|
postalCode: "54340",
|
||||||
|
phone: "03 83 24 00 00",
|
||||||
|
memberSince: 2019,
|
||||||
|
tags: "Cuisine de saison, Brunch, Terrasse, Produits frais",
|
||||||
|
hours: "Lundi – Vendredi|9h – 18h\nSamedi|9h – 19h\nDimanche|10h – 15h",
|
||||||
|
description: "Cuisine de saison, brunch le dimanche, terrasse au cœur du bourg.",
|
||||||
|
status: "active" as const,
|
||||||
|
highlighted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Atelier Émilie",
|
||||||
|
email: "emilie@atelier.fr",
|
||||||
|
categoryId: catId("mode-beaute"),
|
||||||
|
city: "Champigneulles",
|
||||||
|
address: "7 av. des Tilleuls",
|
||||||
|
description: "Salon de coiffure & soins, sur rendez-vous du mardi au samedi.",
|
||||||
|
status: "active" as const,
|
||||||
|
highlighted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Garage Moderne",
|
||||||
|
email: "contact@garagemoderne.fr",
|
||||||
|
categoryId: catId("services"),
|
||||||
|
city: "Custines",
|
||||||
|
description: "Entretien et réparation toutes marques.",
|
||||||
|
status: "active" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Boutique Indigo",
|
||||||
|
email: "indigo@boutique.fr",
|
||||||
|
categoryId: catId("mode-beaute"),
|
||||||
|
city: "Liverdun",
|
||||||
|
description: "Prêt-à-porter et accessoires.",
|
||||||
|
status: "pending" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fleurs & Sens",
|
||||||
|
email: "contact@fleursetsens.fr",
|
||||||
|
categoryId: catId("artisanat"),
|
||||||
|
city: "Pompey",
|
||||||
|
description: "Compositions florales, fleurs de saison cueillies localement.",
|
||||||
|
status: "active" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "La Cave Gourmande",
|
||||||
|
email: "bonjour@cavegourmande.fr",
|
||||||
|
categoryId: catId("alimentation"),
|
||||||
|
city: "Frouard",
|
||||||
|
description: "Fromages fermiers, vins et produits du terroir.",
|
||||||
|
status: "active" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function demoPromotions(memberId: (name: string) => number | null) {
|
||||||
|
return [
|
||||||
|
{ title: "Le 13e pain offert", text: "Sur présentation de votre carte adhérent, profitez d'une réduction sur nos pains au levain.", category: "Boulangerie", badge: "-20%", memberId: memberId("Au Bon Pain"), status: "live" as const, validUntil: "Valable jusqu'au 30 juin" },
|
||||||
|
{ title: "Formule midi du marché", text: "Entrée + plat + café à 15€ tous les midis de semaine. Produits frais et locaux.", category: "Restauration", badge: "Menu 15€", memberId: memberId("Café des Arts"), status: "live" as const, validUntil: "Du lundi au vendredi" },
|
||||||
|
{ title: "Soin offert dès 2 prestations", text: "Profitez de -30% sur le 2e soin réservé dans le mois. Sur rendez-vous.", category: "Beauté", badge: "-30%", memberId: memberId("Atelier Émilie"), status: "pending" as const, validUntil: "Jusqu'au 15 juillet" },
|
||||||
|
{ title: "Révision auto à prix réseau", text: "15€ de remise sur votre forfait révision pour tous les adhérents Plein R.", category: "Services", badge: "-15€", memberId: memberId("Garage Moderne"), status: "pending" as const, validUntil: "Toute l'année" },
|
||||||
|
{ title: "Bouquet du mois en promo", text: "-25% sur la composition florale du mois. Fleurs de saison, cueillies localement.", category: "Artisanat", badge: "-25%", memberId: memberId("Fleurs & Sens"), status: "live" as const, validUntil: "Jusqu'au 30 juin" },
|
||||||
|
{ title: "2 fromages achetés, 1 offert", text: "Sur une sélection de fromages fermiers. L'occasion de découvrir nos producteurs.", category: "Alimentation", badge: "2+1", memberId: memberId("La Cave Gourmande"), status: "live" as const, validUntil: "Ce week-end" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const demoRequests = [
|
||||||
|
{ name: "Boutique Indigo", email: "indigo@boutique.fr", message: "Souhaite rejoindre le réseau." },
|
||||||
|
{ name: "Pizzeria Bella", email: "contact@bella.fr", message: "Demande d'adhésion." },
|
||||||
|
{ name: "Coworking La Ruche", email: "hello@laruche.fr", message: "Espace de travail partagé." },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const demoActivity = [
|
||||||
|
{ dot: "#1f8a5b", message: "<strong>Atelier Émilie</strong> a publié une promotion « Soin offert dès 2 prestations »" },
|
||||||
|
{ dot: "#E0A63C", message: "<strong>Garage Moderne</strong> a soumis une promotion en attente de validation" },
|
||||||
|
{ dot: "#2C6FB3", message: "Nouvelle demande d'adhésion : <strong>Boutique Indigo</strong>" },
|
||||||
|
{ dot: "#9a6638", message: "<strong>Café des Arts</strong> a mis à jour sa fiche annuaire" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function demoUsers(memberId: (name: string) => number | null) {
|
||||||
|
return [
|
||||||
|
{ email: "claire@plein-r.fr", name: "Claire Martin", role: "admin" as const, memberId: null },
|
||||||
|
{ email: "thomas@plein-r.fr", name: "Thomas Petit", role: "moderator" as const, memberId: null },
|
||||||
|
{ email: "sophie@plein-r.fr", name: "Sophie Aubert", role: "editor" as const, memberId: null },
|
||||||
|
{ email: "contact@aubonpain.fr", name: "Au Bon Pain", role: "member" as const, memberId: memberId("Au Bon Pain") },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** E-mails des fiches adhérent de démonstration. */
|
||||||
|
export function demoMemberEmails(): string[] {
|
||||||
|
return demoMembers(() => null).map((m) => m.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Titres des promotions de démonstration. */
|
||||||
|
export function demoPromotionTitles(): string[] {
|
||||||
|
return demoPromotions(() => null).map((p) => p.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** E-mails des comptes de démonstration. */
|
||||||
|
export function demoUserEmails(): string[] {
|
||||||
|
return demoUsers(() => null).map((u) => u.email);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import { and, inArray, isNull } from "drizzle-orm";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
import {
|
||||||
|
demoActivity,
|
||||||
|
demoMemberEmails,
|
||||||
|
demoPromotionTitles,
|
||||||
|
demoRequests,
|
||||||
|
demoUserEmails,
|
||||||
|
} from "./demo-data";
|
||||||
|
|
||||||
|
const { activityLog, imageConsents, meetingRegistrations, members, membershipRequests, promotions, users } = schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retire les données de démonstration d'une base qui les a reçues (anciens
|
||||||
|
* déploiements où le seed les insérait à chaque démarrage).
|
||||||
|
*
|
||||||
|
* Seuls les enregistrements du jeu de démonstration sont touchés, reconnus par
|
||||||
|
* leur e-mail, leur titre, leur nom ou leur message exacts. Tout ce qui a été
|
||||||
|
* saisi par l'association reste en place, y compris les catégories et le
|
||||||
|
* compte administrateur.
|
||||||
|
*
|
||||||
|
* npm run db:purge-demo (poste de développement)
|
||||||
|
* docker compose exec app node dist/purge-demo.cjs (conteneur)
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString) throw new Error("DATABASE_URL is not set");
|
||||||
|
|
||||||
|
const pool = new Pool({ connectionString, max: 1 });
|
||||||
|
const db = drizzle(pool, { schema });
|
||||||
|
|
||||||
|
const memberEmails = demoMemberEmails();
|
||||||
|
const promoTitles = demoPromotionTitles();
|
||||||
|
const userEmails = demoUserEmails();
|
||||||
|
const requestNames = demoRequests.map((r) => r.name);
|
||||||
|
const activityMessages = demoActivity.map((a) => a.message);
|
||||||
|
|
||||||
|
const removed = await db.transaction(async (tx) => {
|
||||||
|
const demoMemberRows = await tx
|
||||||
|
.select({ id: members.id })
|
||||||
|
.from(members)
|
||||||
|
.where(inArray(members.email, memberEmails));
|
||||||
|
const memberIds = demoMemberRows.map((m) => m.id);
|
||||||
|
|
||||||
|
// Comptes de démonstration (staff et adhérent).
|
||||||
|
const deletedUsers = await tx
|
||||||
|
.delete(users)
|
||||||
|
.where(inArray(users.email, userEmails))
|
||||||
|
.returning({ id: users.id });
|
||||||
|
|
||||||
|
let deletedPromos: { id: number }[] = [];
|
||||||
|
let deletedMembers: { id: number }[] = [];
|
||||||
|
if (memberIds.length > 0) {
|
||||||
|
// Un compte réel rattaché par erreur à une fiche de démo est détaché,
|
||||||
|
// pas supprimé.
|
||||||
|
await tx.update(users).set({ memberId: null }).where(inArray(users.memberId, memberIds));
|
||||||
|
await tx.delete(imageConsents).where(inArray(imageConsents.memberId, memberIds));
|
||||||
|
await tx
|
||||||
|
.update(meetingRegistrations)
|
||||||
|
.set({ memberId: null })
|
||||||
|
.where(inArray(meetingRegistrations.memberId, memberIds));
|
||||||
|
// Les publications réseaux liées suivent (clé étrangère en cascade).
|
||||||
|
deletedPromos = await tx
|
||||||
|
.delete(promotions)
|
||||||
|
.where(inArray(promotions.memberId, memberIds))
|
||||||
|
.returning({ id: promotions.id });
|
||||||
|
deletedMembers = await tx
|
||||||
|
.delete(members)
|
||||||
|
.where(inArray(members.id, memberIds))
|
||||||
|
.returning({ id: members.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promotions de démo sans fiche (seed antérieur, fiche déjà retirée).
|
||||||
|
const orphanPromos = await tx
|
||||||
|
.delete(promotions)
|
||||||
|
.where(and(inArray(promotions.title, promoTitles), isNull(promotions.memberId)))
|
||||||
|
.returning({ id: promotions.id });
|
||||||
|
|
||||||
|
const deletedRequests = await tx
|
||||||
|
.delete(membershipRequests)
|
||||||
|
.where(inArray(membershipRequests.name, requestNames))
|
||||||
|
.returning({ id: membershipRequests.id });
|
||||||
|
|
||||||
|
const deletedActivity = await tx
|
||||||
|
.delete(activityLog)
|
||||||
|
.where(inArray(activityLog.message, activityMessages))
|
||||||
|
.returning({ id: activityLog.id });
|
||||||
|
|
||||||
|
return {
|
||||||
|
comptes: deletedUsers.length,
|
||||||
|
adherents: deletedMembers.length,
|
||||||
|
promotions: deletedPromos.length + orphanPromos.length,
|
||||||
|
demandes: deletedRequests.length,
|
||||||
|
journal: deletedActivity.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Données de démonstration retirées :");
|
||||||
|
for (const [key, count] of Object.entries(removed)) console.log(` ${key} : ${count}`);
|
||||||
|
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("Purge failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+47
-140
@@ -5,6 +5,14 @@ import bcrypt from "bcryptjs";
|
|||||||
import { randomInt } from "node:crypto";
|
import { randomInt } from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
|
import {
|
||||||
|
DEMO_PASSWORD,
|
||||||
|
demoActivity,
|
||||||
|
demoMembers,
|
||||||
|
demoPromotions,
|
||||||
|
demoRequests,
|
||||||
|
demoUsers,
|
||||||
|
} from "./demo-data";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
categories,
|
categories,
|
||||||
@@ -126,127 +134,49 @@ async function main() {
|
|||||||
const cats = await db.select().from(categories);
|
const cats = await db.select().from(categories);
|
||||||
const catId = (slug: string) => cats.find((c) => c.slug === slug)?.id ?? null;
|
const catId = (slug: string) => cats.find((c) => c.slug === slug)?.id ?? null;
|
||||||
|
|
||||||
// ---- Members (adhérents) ----
|
// ---- Données de démonstration (SEED_DEMO=true uniquement) ----
|
||||||
const memberData = [
|
//
|
||||||
{
|
// Par défaut la base reste vide : seuls le référentiel des catégories et le
|
||||||
name: "Au Bon Pain",
|
// compte administrateur initial sont créés. Les adhérents, promotions,
|
||||||
email: "contact@aubonpain.fr",
|
// demandes, entrées de journal et comptes de démonstration ne servent qu'à
|
||||||
categoryId: catId("boulangerie"),
|
// un poste de développement.
|
||||||
city: "Frouard",
|
const demo = (process.env.SEED_DEMO ?? "").trim().toLowerCase() === "true";
|
||||||
address: "12 rue de la République",
|
if (demo) {
|
||||||
postalCode: "54390",
|
for (const m of demoMembers(catId)) {
|
||||||
phone: "03 83 49 00 00",
|
const existing = await db.select().from(members).where(eq(members.email, m.email));
|
||||||
website: "https://www.aubonpain-frouard.fr",
|
if (existing.length === 0) await db.insert(members).values(m);
|
||||||
memberSince: 2021,
|
}
|
||||||
tags: "Levain naturel, Produits locaux, Fait maison, Sans conservateur",
|
const allMembers = await db.select().from(members);
|
||||||
hours: "Mardi – Vendredi|7h – 19h30\nSamedi|7h – 19h\nDimanche|7h – 13h\nLundi|Fermé",
|
const memberId = (name: string) => allMembers.find((m) => m.name === name)?.id ?? null;
|
||||||
description:
|
|
||||||
"Boulangerie artisanale familiale installée au cœur de Frouard depuis trois générations. Nous travaillons chaque jour des farines locales et un levain naturel pour vous proposer pains, viennoiseries et pâtisseries faits maison.\nNotre équipe vous accueille du mardi au dimanche matin. Spécialités : pain au levain, baguette de tradition, kouglof et tartes aux fruits de saison.",
|
|
||||||
status: "active" as const,
|
|
||||||
highlighted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Café des Arts",
|
|
||||||
email: "hello@cafedesarts.fr",
|
|
||||||
categoryId: catId("restauration"),
|
|
||||||
city: "Pompey",
|
|
||||||
address: "3 place Stanislas",
|
|
||||||
postalCode: "54340",
|
|
||||||
phone: "03 83 24 00 00",
|
|
||||||
memberSince: 2019,
|
|
||||||
tags: "Cuisine de saison, Brunch, Terrasse, Produits frais",
|
|
||||||
hours: "Lundi – Vendredi|9h – 18h\nSamedi|9h – 19h\nDimanche|10h – 15h",
|
|
||||||
description: "Cuisine de saison, brunch le dimanche, terrasse au cœur du bourg.",
|
|
||||||
status: "active" as const,
|
|
||||||
highlighted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Atelier Émilie",
|
|
||||||
email: "emilie@atelier.fr",
|
|
||||||
categoryId: catId("mode-beaute"),
|
|
||||||
city: "Champigneulles",
|
|
||||||
address: "7 av. des Tilleuls",
|
|
||||||
description: "Salon de coiffure & soins, sur rendez-vous du mardi au samedi.",
|
|
||||||
status: "active" as const,
|
|
||||||
highlighted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Garage Moderne",
|
|
||||||
email: "contact@garagemoderne.fr",
|
|
||||||
categoryId: catId("services"),
|
|
||||||
city: "Custines",
|
|
||||||
description: "Entretien et réparation toutes marques.",
|
|
||||||
status: "active" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Boutique Indigo",
|
|
||||||
email: "indigo@boutique.fr",
|
|
||||||
categoryId: catId("mode-beaute"),
|
|
||||||
city: "Liverdun",
|
|
||||||
description: "Prêt-à-porter et accessoires.",
|
|
||||||
status: "pending" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fleurs & Sens",
|
|
||||||
email: "contact@fleursetsens.fr",
|
|
||||||
categoryId: catId("artisanat"),
|
|
||||||
city: "Pompey",
|
|
||||||
description: "Compositions florales, fleurs de saison cueillies localement.",
|
|
||||||
status: "active" as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "La Cave Gourmande",
|
|
||||||
email: "bonjour@cavegourmande.fr",
|
|
||||||
categoryId: catId("alimentation"),
|
|
||||||
city: "Frouard",
|
|
||||||
description: "Fromages fermiers, vins et produits du terroir.",
|
|
||||||
status: "active" as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const m of memberData) {
|
for (const p of demoPromotions(memberId)) {
|
||||||
const existing = await db.select().from(members).where(eq(members.email, m.email));
|
const existing = await db.select().from(promotions).where(eq(promotions.title, p.title));
|
||||||
if (existing.length === 0) await db.insert(members).values(m);
|
if (existing.length === 0) await db.insert(promotions).values(p);
|
||||||
}
|
}
|
||||||
const allMembers = await db.select().from(members);
|
|
||||||
const memberId = (name: string) => allMembers.find((m) => m.name === name)?.id ?? null;
|
|
||||||
|
|
||||||
// ---- Promotions ----
|
for (const r of demoRequests) {
|
||||||
const promoData = [
|
const existing = await db.select().from(membershipRequests).where(eq(membershipRequests.name, r.name));
|
||||||
{ title: "Le 13e pain offert", text: "Sur présentation de votre carte adhérent, profitez d'une réduction sur nos pains au levain.", category: "Boulangerie", badge: "-20%", memberId: memberId("Au Bon Pain"), status: "live" as const, validUntil: "Valable jusqu'au 30 juin" },
|
if (existing.length === 0) await db.insert(membershipRequests).values(r);
|
||||||
{ title: "Formule midi du marché", text: "Entrée + plat + café à 15€ tous les midis de semaine. Produits frais et locaux.", category: "Restauration", badge: "Menu 15€", memberId: memberId("Café des Arts"), status: "live" as const, validUntil: "Du lundi au vendredi" },
|
}
|
||||||
{ title: "Soin offert dès 2 prestations", text: "Profitez de -30% sur le 2e soin réservé dans le mois. Sur rendez-vous.", category: "Beauté", badge: "-30%", memberId: memberId("Atelier Émilie"), status: "pending" as const, validUntil: "Jusqu'au 15 juillet" },
|
|
||||||
{ title: "Révision auto à prix réseau", text: "15€ de remise sur votre forfait révision pour tous les adhérents Plein R.", category: "Services", badge: "-15€", memberId: memberId("Garage Moderne"), status: "pending" as const, validUntil: "Toute l'année" },
|
|
||||||
{ title: "Bouquet du mois en promo", text: "-25% sur la composition florale du mois. Fleurs de saison, cueillies localement.", category: "Artisanat", badge: "-25%", memberId: memberId("Fleurs & Sens"), status: "live" as const, validUntil: "Jusqu'au 30 juin" },
|
|
||||||
{ title: "2 fromages achetés, 1 offert", text: "Sur une sélection de fromages fermiers. L'occasion de découvrir nos producteurs.", category: "Alimentation", badge: "2+1", memberId: memberId("La Cave Gourmande"), status: "live" as const, validUntil: "Ce week-end" },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const p of promoData) {
|
const existingActivity = await db.select().from(activityLog);
|
||||||
const existing = await db.select().from(promotions).where(eq(promotions.title, p.title));
|
if (existingActivity.length === 0) {
|
||||||
if (existing.length === 0) await db.insert(promotions).values(p);
|
for (const a of demoActivity) await db.insert(activityLog).values(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Membership requests ----
|
for (const u of demoUsers(memberId)) {
|
||||||
const requestData = [
|
const existing = await db.select({ id: users.id }).from(users).where(eq(users.email, u.email));
|
||||||
{ name: "Boutique Indigo", email: "indigo@boutique.fr", message: "Souhaite rejoindre le réseau." },
|
if (existing.length === 0) {
|
||||||
{ name: "Pizzeria Bella", email: "contact@bella.fr", message: "Demande d'adhésion." },
|
await db.insert(users).values({
|
||||||
{ name: "Coworking La Ruche", email: "hello@laruche.fr", message: "Espace de travail partagé." },
|
email: u.email,
|
||||||
];
|
name: u.name,
|
||||||
for (const r of requestData) {
|
passwordHash: await bcrypt.hash(DEMO_PASSWORD, 10),
|
||||||
const existing = await db.select().from(membershipRequests).where(eq(membershipRequests.name, r.name));
|
role: u.role,
|
||||||
if (existing.length === 0) await db.insert(membershipRequests).values(r);
|
memberId: u.memberId,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
// ---- Activity log ----
|
}
|
||||||
const activityData = [
|
console.log(` Données de démonstration insérées (SEED_DEMO=true) ; comptes de démo : mot de passe « ${DEMO_PASSWORD} ».`);
|
||||||
{ dot: "#1f8a5b", message: "<strong>Atelier Émilie</strong> a publié une promotion « Soin offert dès 2 prestations »" },
|
|
||||||
{ dot: "#E0A63C", message: "<strong>Garage Moderne</strong> a soumis une promotion en attente de validation" },
|
|
||||||
{ dot: "#2C6FB3", message: "Nouvelle demande d'adhésion : <strong>Boutique Indigo</strong>" },
|
|
||||||
{ dot: "#9a6638", message: "<strong>Café des Arts</strong> a mis à jour sa fiche annuaire" },
|
|
||||||
];
|
|
||||||
const existingActivity = await db.select().from(activityLog);
|
|
||||||
if (existingActivity.length === 0) {
|
|
||||||
for (const a of activityData) await db.insert(activityLog).values(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Comptes de connexion ----
|
// ---- Comptes de connexion ----
|
||||||
@@ -260,7 +190,6 @@ async function main() {
|
|||||||
const adminEmail = (process.env.SEED_ADMIN_EMAIL ?? "admin@plein-r.fr").trim().toLowerCase();
|
const adminEmail = (process.env.SEED_ADMIN_EMAIL ?? "admin@plein-r.fr").trim().toLowerCase();
|
||||||
const adminName = process.env.SEED_ADMIN_NAME ?? "Administrateur Plein R";
|
const adminName = process.env.SEED_ADMIN_NAME ?? "Administrateur Plein R";
|
||||||
const providedAdminPassword = (process.env.SEED_ADMIN_PASSWORD ?? "").trim();
|
const providedAdminPassword = (process.env.SEED_ADMIN_PASSWORD ?? "").trim();
|
||||||
const demo = (process.env.SEED_DEMO ?? "").trim().toLowerCase() === "true";
|
|
||||||
|
|
||||||
const [existingAdmin] = await db.select({ id: users.id }).from(users).where(eq(users.email, adminEmail));
|
const [existingAdmin] = await db.select({ id: users.id }).from(users).where(eq(users.email, adminEmail));
|
||||||
if (!existingAdmin) {
|
if (!existingAdmin) {
|
||||||
@@ -283,28 +212,6 @@ async function main() {
|
|||||||
console.log(" Un changement de mot de passe sera exigé à la première connexion.");
|
console.log(" Un changement de mot de passe sera exigé à la première connexion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (demo) {
|
|
||||||
const demoUsers = [
|
|
||||||
{ email: "claire@plein-r.fr", name: "Claire Martin", role: "admin" as const, memberId: null },
|
|
||||||
{ email: "thomas@plein-r.fr", name: "Thomas Petit", role: "moderator" as const, memberId: null },
|
|
||||||
{ email: "sophie@plein-r.fr", name: "Sophie Aubert", role: "editor" as const, memberId: null },
|
|
||||||
{ email: "contact@aubonpain.fr", name: "Au Bon Pain", role: "member" as const, memberId: memberId("Au Bon Pain") },
|
|
||||||
];
|
|
||||||
for (const u of demoUsers) {
|
|
||||||
const existing = await db.select({ id: users.id }).from(users).where(eq(users.email, u.email));
|
|
||||||
if (existing.length === 0) {
|
|
||||||
await db.insert(users).values({
|
|
||||||
email: u.email,
|
|
||||||
name: u.name,
|
|
||||||
passwordHash: await bcrypt.hash("changeme123", 10),
|
|
||||||
role: u.role,
|
|
||||||
memberId: u.memberId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(" Comptes de démonstration créés (SEED_DEMO=true) : mot de passe « changeme123 ».");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seed complete.");
|
console.log("Seed complete.");
|
||||||
await pool.end();
|
await pool.end();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user