diff --git a/package-lock.json b/package-lock.json index fe7d18a7..68f87815 100755 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.0", - "cross-env": "^10.1.0", + "cross-env": "^7.0.3", "eslint": "^9.39.0", "jest": "^30.2.0", "nodemon": "^3.1.10", @@ -568,13 +568,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2636,21 +2629,22 @@ } }, "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" + "cross-spawn": "^7.0.1" }, "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" }, "engines": { - "node": ">=20" + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" } }, "node_modules/cross-spawn": { @@ -4916,12 +4910,12 @@ } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -4949,12 +4943,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, diff --git a/package.json b/package.json index e8a737b1..ea0c622a 100755 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.0", - "cross-env": "^10.1.0", + "cross-env": "^7.0.3", "eslint": "^9.39.0", "jest": "^30.2.0", "nodemon": "^3.1.10", diff --git a/scripts/auto-init-db.js b/scripts/auto-init-db.js new file mode 100644 index 00000000..a10026cd --- /dev/null +++ b/scripts/auto-init-db.js @@ -0,0 +1,369 @@ +/** + * 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 { + 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 { + 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 { + // Add is_active column to users if not exists + await pool.query(` + ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE + `); + console.log(' ✅ Colonne is_active ajoutée à users'); + + // 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)); +}