Compare commits
5 Commits
17a9dc7b22
...
26914469d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 26914469d3 | |||
| 9c9956b068 | |||
| a7f82b1215 | |||
| 62bb12f3a7 | |||
| a9035357ec |
|
|
@ -148,9 +148,12 @@ CREATE TRIGGER update_game_settings_updated_at BEFORE UPDATE ON game_settings
|
||||||
-- INITIAL DATA
|
-- INITIAL DATA
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
-- Configuration du jeu (concours du 1er janvier au 30 septembre 2025)
|
-- Configuration du jeu-concours Thé Tip Top
|
||||||
|
-- Période de participation : 1 décembre 2025 au 31 décembre 2025 (30 jours)
|
||||||
|
-- Période de récupération des lots : jusqu'au 31 janvier 2026 (60 jours)
|
||||||
|
-- Tirage au sort : 1 février 2026
|
||||||
INSERT INTO game_settings (start_date, end_date, is_active, total_tickets)
|
INSERT INTO game_settings (start_date, end_date, is_active, total_tickets)
|
||||||
VALUES ('2025-01-01 00:00:00', '2025-09-30 23:59:59', TRUE, 500000);
|
VALUES ('2025-12-01 00:00:00', '2025-12-31 23:59:59', TRUE, 500000);
|
||||||
|
|
||||||
-- Prix avec probabilités respectant la distribution demandée
|
-- Prix avec probabilités respectant la distribution demandée
|
||||||
-- Total: 500,000 tickets
|
-- Total: 500,000 tickets
|
||||||
|
|
|
||||||
70
scripts/compare-db.js
Normal file
70
scripts/compare-db.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import pkg from 'pg';
|
||||||
|
const { Pool } = pkg;
|
||||||
|
|
||||||
|
const devPool = new Pool({
|
||||||
|
host: '51.75.24.29',
|
||||||
|
port: 5433,
|
||||||
|
user: 'postgres',
|
||||||
|
password: 'postgres',
|
||||||
|
database: 'thetiptop_dev'
|
||||||
|
});
|
||||||
|
|
||||||
|
const preprodPool = new Pool({
|
||||||
|
host: '51.75.24.29',
|
||||||
|
port: 5434,
|
||||||
|
user: 'postgres',
|
||||||
|
password: 'postgres',
|
||||||
|
database: 'thetiptop_preprod'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tables = ['users', 'prizes', 'tickets', 'game_settings', 'newsletters', 'email_templates', 'email_campaigns', 'email_campaign_recipients', 'grand_prize_draws'];
|
||||||
|
|
||||||
|
console.log('=== COMPARAISON DES COLONNES DEV vs PREPROD ===\n');
|
||||||
|
|
||||||
|
const missingColumns = [];
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
const devCols = await devPool.query(
|
||||||
|
"SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position",
|
||||||
|
[table]
|
||||||
|
);
|
||||||
|
|
||||||
|
const preprodCols = await preprodPool.query(
|
||||||
|
"SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position",
|
||||||
|
[table]
|
||||||
|
);
|
||||||
|
|
||||||
|
const devSet = new Set(devCols.rows.map(r => r.column_name));
|
||||||
|
const preprodSet = new Set(preprodCols.rows.map(r => r.column_name));
|
||||||
|
|
||||||
|
const missingInPreprod = [...devSet].filter(c => !preprodSet.has(c));
|
||||||
|
const extraInPreprod = [...preprodSet].filter(c => !devSet.has(c));
|
||||||
|
|
||||||
|
if (missingInPreprod.length > 0 || extraInPreprod.length > 0) {
|
||||||
|
console.log('❌ ' + table.toUpperCase());
|
||||||
|
if (missingInPreprod.length > 0) {
|
||||||
|
console.log(' Manquantes en preprod: ' + missingInPreprod.join(', '));
|
||||||
|
missingColumns.push({ table, columns: missingInPreprod });
|
||||||
|
}
|
||||||
|
if (extraInPreprod.length > 0) {
|
||||||
|
console.log(' En plus en preprod: ' + extraInPreprod.join(', '));
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
} else {
|
||||||
|
console.log('✅ ' + table.toUpperCase() + ' - OK');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== RÉSUMÉ ===');
|
||||||
|
if (missingColumns.length === 0) {
|
||||||
|
console.log('✅ Toutes les colonnes sont synchronisées !');
|
||||||
|
} else {
|
||||||
|
console.log('❌ Colonnes manquantes à ajouter en preprod:');
|
||||||
|
for (const m of missingColumns) {
|
||||||
|
console.log(' - ' + m.table + ': ' + m.columns.join(', '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await devPool.end();
|
||||||
|
await preprodPool.end();
|
||||||
|
process.exit(0);
|
||||||
140
scripts/fix-preprod-schema.js
Normal file
140
scripts/fix-preprod-schema.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import pkg from 'pg';
|
||||||
|
const { Pool } = pkg;
|
||||||
|
|
||||||
|
const preprodPool = new Pool({
|
||||||
|
host: '51.75.24.29',
|
||||||
|
port: 5434,
|
||||||
|
user: 'postgres',
|
||||||
|
password: 'postgres',
|
||||||
|
database: 'thetiptop_preprod'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== CORRECTION DU SCHÉMA PREPROD ===\n');
|
||||||
|
|
||||||
|
// 1. TICKETS - ajouter colonnes de livraison
|
||||||
|
console.log('📦 Correction de TICKETS...');
|
||||||
|
await preprodPool.query(`
|
||||||
|
ALTER TABLE tickets
|
||||||
|
ADD COLUMN IF NOT EXISTS delivered_at TIMESTAMP,
|
||||||
|
ADD COLUMN IF NOT EXISTS delivered_by UUID REFERENCES users(id),
|
||||||
|
ADD COLUMN IF NOT EXISTS delivery_notes TEXT
|
||||||
|
`);
|
||||||
|
console.log('✅ tickets: delivered_at, delivered_by, delivery_notes ajoutées\n');
|
||||||
|
|
||||||
|
// 2. NEWSLETTERS - corriger is_active
|
||||||
|
console.log('📦 Correction de NEWSLETTERS...');
|
||||||
|
await preprodPool.query(`ALTER TABLE newsletters RENAME COLUMN is_subscribed TO is_active`);
|
||||||
|
console.log('✅ newsletters: is_subscribed renommée en is_active\n');
|
||||||
|
|
||||||
|
// 3. EMAIL_TEMPLATES - recréer avec bonnes colonnes
|
||||||
|
console.log('📦 Correction de EMAIL_TEMPLATES...');
|
||||||
|
await preprodPool.query(`DROP TABLE IF EXISTS email_templates CASCADE`);
|
||||||
|
await preprodPool.query(`
|
||||||
|
CREATE TABLE email_templates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
subject VARCHAR(500),
|
||||||
|
html_content TEXT NOT NULL,
|
||||||
|
text_content TEXT,
|
||||||
|
category VARCHAR(100),
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('✅ email_templates recréée avec toutes les colonnes\n');
|
||||||
|
|
||||||
|
// 4. EMAIL_CAMPAIGNS - recréer avec bonnes colonnes
|
||||||
|
console.log('📦 Correction de EMAIL_CAMPAIGNS...');
|
||||||
|
await preprodPool.query(`DROP TABLE IF EXISTS email_campaign_recipients CASCADE`);
|
||||||
|
await preprodPool.query(`DROP TABLE IF EXISTS email_campaigns CASCADE`);
|
||||||
|
await preprodPool.query(`
|
||||||
|
CREATE TABLE email_campaigns (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
subject VARCHAR(500) NOT NULL,
|
||||||
|
template_html TEXT NOT NULL,
|
||||||
|
template_text TEXT,
|
||||||
|
created_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
status VARCHAR(50) DEFAULT 'DRAFT',
|
||||||
|
scheduled_at TIMESTAMP,
|
||||||
|
sent_at TIMESTAMP,
|
||||||
|
recipient_count INTEGER DEFAULT 0,
|
||||||
|
opened_count INTEGER DEFAULT 0,
|
||||||
|
clicked_count INTEGER DEFAULT 0,
|
||||||
|
criteria JSONB,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('✅ email_campaigns recréée avec toutes les colonnes\n');
|
||||||
|
|
||||||
|
// 5. EMAIL_CAMPAIGN_RECIPIENTS - recréer avec bonnes colonnes
|
||||||
|
console.log('📦 Correction de EMAIL_CAMPAIGN_RECIPIENTS...');
|
||||||
|
await preprodPool.query(`
|
||||||
|
CREATE TABLE email_campaign_recipients (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
campaign_id UUID NOT NULL REFERENCES email_campaigns(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'PENDING',
|
||||||
|
sent_at TIMESTAMP,
|
||||||
|
opened_at TIMESTAMP,
|
||||||
|
clicked_at TIMESTAMP,
|
||||||
|
unsubscribed_at TIMESTAMP,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('✅ email_campaign_recipients recréée avec toutes les colonnes\n');
|
||||||
|
|
||||||
|
// 6. GRAND_PRIZE_DRAWS - recréer avec bonnes colonnes
|
||||||
|
console.log('📦 Correction de GRAND_PRIZE_DRAWS...');
|
||||||
|
await preprodPool.query(`DROP TABLE IF EXISTS grand_prize_draws CASCADE`);
|
||||||
|
await preprodPool.query(`
|
||||||
|
CREATE TABLE grand_prize_draws (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
draw_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
conducted_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
winner_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
winner_email VARCHAR(255) NOT NULL,
|
||||||
|
winner_name VARCHAR(255) NOT NULL,
|
||||||
|
prize_name VARCHAR(255) NOT NULL,
|
||||||
|
prize_value VARCHAR(100),
|
||||||
|
total_participants INTEGER NOT NULL,
|
||||||
|
eligible_participants INTEGER NOT NULL,
|
||||||
|
criteria JSONB,
|
||||||
|
status VARCHAR(50) DEFAULT 'COMPLETED',
|
||||||
|
notified_at TIMESTAMP,
|
||||||
|
claimed_at TIMESTAMP,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unique_grand_prize_draw UNIQUE (draw_date)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('✅ grand_prize_draws recréée avec toutes les colonnes\n');
|
||||||
|
|
||||||
|
// Créer les index
|
||||||
|
console.log('📦 Création des index...');
|
||||||
|
await preprodPool.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaigns_status ON email_campaigns(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaigns_created_by ON email_campaigns(created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaigns_scheduled_at ON email_campaigns(scheduled_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_campaign ON email_campaign_recipients(campaign_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_user ON email_campaign_recipients(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_status ON email_campaign_recipients(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_templates_category ON email_templates(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_templates_is_active ON email_templates(is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_winner ON grand_prize_draws(winner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_date ON grand_prize_draws(draw_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_status ON grand_prize_draws(status);
|
||||||
|
`);
|
||||||
|
console.log('✅ Index créés\n');
|
||||||
|
|
||||||
|
console.log('=== CORRECTION TERMINÉE ===');
|
||||||
|
|
||||||
|
await preprodPool.end();
|
||||||
|
process.exit(0);
|
||||||
|
|
@ -19,6 +19,8 @@ export const getStatistics = asyncHandler(async (req, res) => {
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_users,
|
COUNT(*) as total_users,
|
||||||
COUNT(CASE WHEN role = 'CLIENT' THEN 1 END) as clients,
|
COUNT(CASE WHEN role = 'CLIENT' THEN 1 END) as clients,
|
||||||
|
COUNT(CASE WHEN role = 'CLIENT' AND is_active = TRUE THEN 1 END) as active_clients,
|
||||||
|
COUNT(CASE WHEN role = 'CLIENT' AND is_active = FALSE THEN 1 END) as inactive_clients,
|
||||||
COUNT(CASE WHEN role = 'EMPLOYEE' THEN 1 END) as employees,
|
COUNT(CASE WHEN role = 'EMPLOYEE' THEN 1 END) as employees,
|
||||||
COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) as admins,
|
COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) as admins,
|
||||||
COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_users
|
COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_users
|
||||||
|
|
@ -154,6 +156,8 @@ export const getStatistics = asyncHandler(async (req, res) => {
|
||||||
users: {
|
users: {
|
||||||
total: parseInt(usersStats.rows[0].total_users),
|
total: parseInt(usersStats.rows[0].total_users),
|
||||||
clients: parseInt(usersStats.rows[0].clients),
|
clients: parseInt(usersStats.rows[0].clients),
|
||||||
|
activeClients: parseInt(usersStats.rows[0].active_clients),
|
||||||
|
inactiveClients: parseInt(usersStats.rows[0].inactive_clients),
|
||||||
employees: parseInt(usersStats.rows[0].employees),
|
employees: parseInt(usersStats.rows[0].employees),
|
||||||
admins: parseInt(usersStats.rows[0].admins),
|
admins: parseInt(usersStats.rows[0].admins),
|
||||||
verifiedEmails: parseInt(usersStats.rows[0].verified_users)
|
verifiedEmails: parseInt(usersStats.rows[0].verified_users)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { pool } from '../../db.js';
|
import { pool } from '../../db.js';
|
||||||
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
||||||
import { generateToken, generateJWT, getTokenExpiry, isExpired } from '../utils/helpers.js';
|
import { generateToken, generateJWT, getTokenExpiry, isExpired, isValidEmail } from '../utils/helpers.js';
|
||||||
import { sendResetPasswordEmail, sendWelcomeEmail } from '../services/email.service.js';
|
import { sendResetPasswordEmail, sendWelcomeEmail } from '../services/email.service.js';
|
||||||
import dns from 'dns';
|
import dns from 'dns';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
@ -88,6 +88,7 @@ export const register = asyncHandler(async (req, res, next) => {
|
||||||
firstName: user.first_name,
|
firstName: user.first_name,
|
||||||
lastName: user.last_name,
|
lastName: user.last_name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
createdAt: user.created_at,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -101,7 +102,7 @@ export const login = asyncHandler(async (req, res, next) => {
|
||||||
|
|
||||||
// Récupérer l'utilisateur
|
// Récupérer l'utilisateur
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
'SELECT id, email, password, first_name, last_name, role, is_verified, is_active FROM users WHERE email = $1',
|
'SELECT id, email, password, first_name, last_name, role, is_verified, is_active, created_at FROM users WHERE email = $1',
|
||||||
[email]
|
[email]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -137,6 +138,7 @@ export const login = asyncHandler(async (req, res, next) => {
|
||||||
lastName: user.last_name,
|
lastName: user.last_name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
isVerified: user.is_verified,
|
isVerified: user.is_verified,
|
||||||
|
createdAt: user.created_at,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -328,9 +330,8 @@ export const checkEmail = asyncHandler(async (req, res, next) => {
|
||||||
return next(new AppError('Email requis', 400));
|
return next(new AppError('Email requis', 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation format email (regex sécurisée contre ReDoS avec limite de longueur)
|
// Validation format email (utilise isValidEmail pour éviter DoS)
|
||||||
const isValidEmail = email.length <= 254 && /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email);
|
if (!isValidEmail(email)) {
|
||||||
if (!isValidEmail) {
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* Contrôleur pour gérer les messages de contact
|
* Contrôleur pour gérer les messages de contact
|
||||||
*/
|
*/
|
||||||
import { sendContactEmail } from '../services/email.service.js';
|
import { sendContactEmail } from '../services/email.service.js';
|
||||||
|
import { isValidEmail } from '../utils/helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/contact
|
* POST /api/contact
|
||||||
|
|
@ -19,9 +20,8 @@ export const submitContactForm = async (req, res) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation email
|
// Validation email (utilise isValidEmail pour éviter DoS)
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
if (!isValidEmail(email)) {
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Adresse email invalide'
|
message: 'Adresse email invalide'
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,13 @@ export const generateTicketCode = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valide le format d'un email
|
* Valide le format d'un email
|
||||||
|
* Utilise une regex sécurisée (non-backtracking) pour éviter les attaques DoS
|
||||||
*/
|
*/
|
||||||
export const isValidEmail = (email) => {
|
export const isValidEmail = (email) => {
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
// Limite la longueur pour éviter les attaques
|
||||||
|
if (!email || email.length > 254) return false;
|
||||||
|
// Regex simple et sécurisée (non-backtracking)
|
||||||
|
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ export const subscribeSchema = z.object({
|
||||||
.string({
|
.string({
|
||||||
required_error: 'L\'email est requis',
|
required_error: 'L\'email est requis',
|
||||||
})
|
})
|
||||||
.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Format d\'email invalide'),
|
.email('Format d\'email invalide')
|
||||||
|
.max(254, 'Email trop long'),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -21,7 +22,8 @@ export const unsubscribeSchema = z.object({
|
||||||
.string({
|
.string({
|
||||||
required_error: 'L\'email est requis',
|
required_error: 'L\'email est requis',
|
||||||
})
|
})
|
||||||
.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Format d\'email invalide'),
|
.email('Format d\'email invalide')
|
||||||
|
.max(254, 'Email trop long'),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user