From dfe2dfa7edb8e447d07fe7feb6703f0cee02e70a Mon Sep 17 00:00:00 2001 From: soufiane Date: Wed, 3 Dec 2025 23:47:42 +0100 Subject: [PATCH] feat: add automatic database initialization on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create auto-init-db.js script that checks and initializes database - Creates tables from schema.sql if not exist - Creates default admin and employee accounts - Generates 500,000 tickets with proper distribution - Applies migrations for newsletter and email campaigns - Runs automatically when backend starts đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- index.js | 19 ++- scripts/auto-init-db.js | 363 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 scripts/auto-init-db.js diff --git a/index.js b/index.js index 89c38137..10d3d0b3 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ import config from "./src/config/env.js"; import { pool } from "./db.js"; import { errorHandler } from "./src/middleware/errorHandler.js"; import { metricsMiddleware } from "./src/middleware/metrics.js"; +import { initDatabase } from "./scripts/auto-init-db.js"; // Import routes import authRoutes from "./src/routes/auth.routes.js"; @@ -107,7 +108,19 @@ export default app; // Lancement serveur (seulement si pas importĂ© par les tests) if (process.env.NODE_ENV !== 'test') { const PORT = config.server.port; - app.listen(PORT, "0.0.0.0", () => { - console.log(`🚀 Backend lancĂ© sur 0.0.0.0:${PORT} ✅`); - }); + + // Initialiser la base de donnĂ©es avant de lancer le serveur + initDatabase() + .then(() => { + app.listen(PORT, "0.0.0.0", () => { + console.log(`🚀 Backend lancĂ© sur 0.0.0.0:${PORT} ✅`); + }); + }) + .catch((error) => { + console.error('❌ Erreur lors de l\'initialisation de la base de donnĂ©es:', error); + // Lancer le serveur quand mĂȘme pour permettre le debug + app.listen(PORT, "0.0.0.0", () => { + console.log(`🚀 Backend lancĂ© sur 0.0.0.0:${PORT} (sans init DB) ⚠`); + }); + }); } \ No newline at end of file diff --git a/scripts/auto-init-db.js b/scripts/auto-init-db.js new file mode 100644 index 00000000..de1e419a --- /dev/null +++ b/scripts/auto-init-db.js @@ -0,0 +1,363 @@ +/** + * Script d'auto-initialisation de la base de donnĂ©es + * AppelĂ© automatiquement au dĂ©marrage du backend + * + * Ce script vĂ©rifie si les tables existent et les crĂ©e si nĂ©cessaire + * Il crĂ©e Ă©galement les comptes admin/employĂ© et gĂ©nĂšre les 500,000 tickets + */ +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import bcrypt from 'bcrypt'; +import { pool } from '../db.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const SALT_ROUNDS = 10; +const TOTAL_TICKETS = 500000; +const BATCH_SIZE = 5000; + +const PRIZE_DISTRIBUTION = { + 'INFUSEUR': { percentage: 0.60, name: 'Infuseur Ă  thĂ©' }, + 'THE_GRATUIT': { percentage: 0.20, name: 'ThĂ© dĂ©tox/infusion 100g' }, + 'THE_SIGNATURE': { percentage: 0.10, name: 'ThĂ© signature 100g' }, + 'COFFRET_DECOUVERTE': { percentage: 0.06, name: 'Coffret dĂ©couverte 39€' }, + 'COFFRET_PRESTIGE': { percentage: 0.04, name: 'Coffret prestige 69€' } +}; + +/** + * VĂ©rifie si les tables existent dans la base de donnĂ©es + */ +async function tablesExist() { + try { + const result = await pool.query(` + SELECT COUNT(*) as count + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ('users', 'prizes', 'tickets', 'game_settings') + `); + return parseInt(result.rows[0].count) === 4; + } catch (error) { + console.error('❌ Erreur lors de la vĂ©rification des tables:', error.message); + return false; + } +} + +/** + * VĂ©rifie si les tickets ont Ă©tĂ© gĂ©nĂ©rĂ©s + */ +async function ticketsExist() { + try { + const result = await pool.query('SELECT COUNT(*) as count FROM tickets'); + return parseInt(result.rows[0].count) >= TOTAL_TICKETS; + } catch (error) { + return false; + } +} + +/** + * VĂ©rifie si les utilisateurs admin/employĂ© existent + */ +async function usersExist() { + try { + const result = await pool.query(` + SELECT COUNT(*) as count FROM users + WHERE email IN ('admin@thetiptop.com', 'employee1@thetiptop.com') + `); + return parseInt(result.rows[0].count) >= 2; + } catch (error) { + return false; + } +} + +/** + * CrĂ©e les tables Ă  partir du schema.sql + */ +async function createTables() { + console.log('📩 CrĂ©ation des tables...'); + + const schemaPath = join(__dirname, '..', 'database', 'schema.sql'); + const schema = readFileSync(schemaPath, 'utf-8'); + + await pool.query(schema); + console.log('✅ Tables créées avec succĂšs'); +} + +/** + * CrĂ©e les utilisateurs par dĂ©faut (admin, employĂ©s) + */ +async function createDefaultUsers() { + console.log('đŸ‘„ CrĂ©ation des utilisateurs par dĂ©faut...'); + + const adminPassword = await bcrypt.hash('Admin123!', SALT_ROUNDS); + const employeePassword = await bcrypt.hash('Employee123!', SALT_ROUNDS); + + // Admin + await pool.query( + `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) + VALUES ($1, $2, $3, $4, $5, 'ADMIN', TRUE) + ON CONFLICT (email) DO UPDATE SET + password = EXCLUDED.password, + role = 'ADMIN', + is_verified = TRUE`, + ['admin@thetiptop.com', adminPassword, 'Admin', 'Principal', '+33123456789'] + ); + console.log(' ✅ Admin créé: admin@thetiptop.com'); + + // EmployĂ© 1 + await pool.query( + `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) + VALUES ($1, $2, $3, $4, $5, 'EMPLOYEE', TRUE) + ON CONFLICT (email) DO UPDATE SET + password = EXCLUDED.password, + role = 'EMPLOYEE', + is_verified = TRUE`, + ['employee1@thetiptop.com', employeePassword, 'Marie', 'Dupont', '+33198765432'] + ); + console.log(' ✅ EmployĂ© créé: employee1@thetiptop.com'); + + // EmployĂ© 2 + await pool.query( + `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) + VALUES ($1, $2, $3, $4, $5, 'EMPLOYEE', TRUE) + ON CONFLICT (email) DO UPDATE SET + password = EXCLUDED.password, + role = 'EMPLOYEE', + is_verified = TRUE`, + ['employee2@thetiptop.com', employeePassword, 'Pierre', 'Martin', '+33187654321'] + ); + console.log(' ✅ EmployĂ© créé: employee2@thetiptop.com'); + + console.log('✅ Utilisateurs créés avec succĂšs'); +} + +/** + * GĂ©nĂšre un code de ticket unique + */ +function generateCode() { + const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let code = 'TT'; + for (let i = 0; i < 8; i++) { + code += chars[Math.floor(Math.random() * chars.length)]; + } + return code; +} + +/** + * GĂ©nĂšre les 500,000 tickets + */ +async function generateTickets() { + console.log('đŸŽ« GĂ©nĂ©ration des 500,000 tickets...'); + + const prizesResult = await pool.query('SELECT id, name, type FROM prizes'); + const prizes = {}; + prizesResult.rows.forEach(p => prizes[p.type] = p); + + // Calculer la distribution + const distribution = {}; + let total = 0; + + for (const [type, config] of Object.entries(PRIZE_DISTRIBUTION)) { + const count = Math.floor(TOTAL_TICKETS * config.percentage); + distribution[type] = count; + total += count; + } + + // Ajuster si nĂ©cessaire + if (total < TOTAL_TICKETS) { + distribution['INFUSEUR'] += (TOTAL_TICKETS - total); + } + + // Afficher la distribution + console.log(' 📊 Distribution:'); + for (const [type, count] of Object.entries(distribution)) { + console.log(` - ${type}: ${count.toLocaleString('fr-FR')} tickets`); + } + + // GĂ©nĂ©rer les tickets + let generated = 0; + const start = Date.now(); + + for (const [type, count] of Object.entries(distribution)) { + const prize = prizes[type]; + if (!prize) { + console.log(` ⚠ Lot "${type}" introuvable, ignorĂ©`); + continue; + } + + for (let i = 0; i < count; i += BATCH_SIZE) { + const batch = Math.min(BATCH_SIZE, count - i); + const values = []; + + for (let j = 0; j < batch; j++) { + const code = generateCode(); + values.push(`('${code}', '${prize.id}', NULL)`); + } + + const query = `INSERT INTO tickets (code, prize_id, status) VALUES ${values.join(', ')}`; + await pool.query(query); + generated += batch; + + // Afficher la progression tous les 50,000 tickets + if (generated % 50000 === 0) { + console.log(` 📈 Progression: ${generated.toLocaleString('fr-FR')} / ${TOTAL_TICKETS.toLocaleString('fr-FR')}`); + } + } + } + + const duration = ((Date.now() - start) / 1000).toFixed(2); + console.log(`✅ ${generated.toLocaleString('fr-FR')} tickets gĂ©nĂ©rĂ©s en ${duration}s`); +} + +/** + * Applique les migrations (newsletter, email campaigns, etc.) + */ +async function applyMigrations() { + console.log('🔄 Application des migrations...'); + + try { + // Newsletter table + await pool.query(` + CREATE TABLE IF NOT EXISTS newsletters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + is_subscribed BOOLEAN DEFAULT TRUE, + subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + unsubscribed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log(' ✅ Table newsletters créée'); + + // Email templates table + await pool.query(` + CREATE TABLE IF NOT EXISTS email_templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + subject VARCHAR(500) NOT NULL, + html_content TEXT NOT NULL, + text_content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log(' ✅ Table email_templates créée'); + + // Email campaigns table + await pool.query(` + CREATE TABLE IF NOT EXISTS email_campaigns ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + template_id UUID REFERENCES email_templates(id), + status VARCHAR(50) DEFAULT 'DRAFT', + scheduled_at TIMESTAMP, + sent_at TIMESTAMP, + total_recipients INTEGER DEFAULT 0, + sent_count INTEGER DEFAULT 0, + failed_count INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log(' ✅ Table email_campaigns créée'); + + // Email campaign recipients table + await pool.query(` + CREATE TABLE IF NOT EXISTS email_campaign_recipients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + campaign_id UUID REFERENCES email_campaigns(id) ON DELETE CASCADE, + email VARCHAR(255) NOT NULL, + status VARCHAR(50) DEFAULT 'PENDING', + sent_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log(' ✅ Table email_campaign_recipients créée'); + + // Grand prize draws table + await pool.query(` + CREATE TABLE IF NOT EXISTS grand_prize_draws ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + winner_id UUID REFERENCES users(id), + draw_date TIMESTAMP NOT NULL, + prize_description TEXT, + is_claimed BOOLEAN DEFAULT FALSE, + claimed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log(' ✅ Table grand_prize_draws créée'); + + console.log('✅ Migrations appliquĂ©es avec succĂšs'); + } catch (error) { + console.log(' ⚠ Certaines migrations existent dĂ©jĂ :', error.message); + } +} + +/** + * Fonction principale d'initialisation + */ +export async function initDatabase() { + console.log('\n🚀 ========================================'); + console.log(' AUTO-INITIALISATION DE LA BASE DE DONNÉES'); + console.log(' ========================================\n'); + + try { + // 1. VĂ©rifier si les tables existent + const hasTables = await tablesExist(); + + if (!hasTables) { + console.log('📋 Tables non trouvĂ©es, crĂ©ation en cours...\n'); + await createTables(); + } else { + console.log('✅ Tables dĂ©jĂ  existantes\n'); + } + + // 2. VĂ©rifier si les utilisateurs existent + const hasUsers = await usersExist(); + + if (!hasUsers) { + console.log('đŸ‘„ Utilisateurs non trouvĂ©s, crĂ©ation en cours...\n'); + await createDefaultUsers(); + } else { + console.log('✅ Utilisateurs dĂ©jĂ  existants\n'); + } + + // 3. VĂ©rifier si les tickets existent + const hasTickets = await ticketsExist(); + + if (!hasTickets) { + console.log('đŸŽ« Tickets non trouvĂ©s, gĂ©nĂ©ration en cours...\n'); + await generateTickets(); + } else { + console.log('✅ Tickets dĂ©jĂ  gĂ©nĂ©rĂ©s\n'); + } + + // 4. Appliquer les migrations + await applyMigrations(); + + console.log('\n✹ ========================================'); + console.log(' BASE DE DONNÉES INITIALISÉE AVEC SUCCÈS'); + console.log(' ========================================'); + console.log('\n🔐 Comptes disponibles:'); + console.log(' Admin: admin@thetiptop.com / Admin123!'); + console.log(' EmployĂ© 1: employee1@thetiptop.com / Employee123!'); + console.log(' EmployĂ© 2: employee2@thetiptop.com / Employee123!\n'); + + return true; + } catch (error) { + console.error('\n❌ Erreur lors de l\'initialisation:', error.message); + console.error(error.stack); + return false; + } +} + +// Si exĂ©cutĂ© directement (pas importĂ©) +if (process.argv[1] === fileURLToPath(import.meta.url)) { + initDatabase() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +}