Files
LogiFlow/server/initDatabase.production.ts
T
2025-08-11 14:43:48 +00:00

1075 lines
49 KiB
TypeScript

import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: false,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
export async function initDatabase() {
try {
console.log('🔧 Initializing production database...');
// Test connection
const client = await pool.connect();
await client.query('SELECT NOW()');
client.release();
console.log('✅ Database connection successful');
// Check if tables exist, if not create them (first time setup)
await createTablesIfNotExist();
// Run incremental migrations to update existing tables
await runMigrations();
// Create default admin user only if it doesn't exist
await createDefaultAdmin();
// Initialize roles and permissions for production
await initRolesAndPermissionsProduction();
console.log('✅ Database initialization complete');
} catch (error) {
console.error('❌ Database initialization failed:', error);
throw error;
}
}
async function createTablesIfNotExist() {
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
name VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
profile_image_url TEXT,
password VARCHAR(255),
role VARCHAR(50) DEFAULT 'employee',
password_changed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createGroupsTable = `
CREATE TABLE IF NOT EXISTS groups (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
color VARCHAR(20) DEFAULT '#1976D2',
nocodb_config_id INTEGER,
nocodb_table_id VARCHAR(255),
nocodb_table_name VARCHAR(255),
invoice_column_name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createSuppliersTable = `
CREATE TABLE IF NOT EXISTS suppliers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
contact VARCHAR(255),
phone VARCHAR(255),
has_dlc BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createOrdersTable = `
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
supplier_id INTEGER REFERENCES suppliers(id) ON DELETE CASCADE,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
planned_date DATE NOT NULL,
quantity INTEGER,
unit VARCHAR(50),
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'planned', 'delivered')),
notes TEXT,
created_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createDeliveriesTable = `
CREATE TABLE IF NOT EXISTS deliveries (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id) ON DELETE SET NULL,
supplier_id INTEGER REFERENCES suppliers(id) ON DELETE CASCADE,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
scheduled_date DATE NOT NULL,
delivered_date TIMESTAMP,
quantity INTEGER NOT NULL,
unit VARCHAR(50) NOT NULL,
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'delivered')),
notes TEXT,
bl_number VARCHAR(255),
bl_amount DECIMAL(10,2),
invoice_reference VARCHAR(255),
invoice_amount DECIMAL(10,2),
reconciled BOOLEAN DEFAULT false,
validated_at TIMESTAMP,
created_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createUserGroupsTable = `
CREATE TABLE IF NOT EXISTS user_groups (
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, group_id)
);
`;
const createSessionTable = `
CREATE TABLE IF NOT EXISTS session (
sid VARCHAR NOT NULL COLLATE "default" PRIMARY KEY,
sess JSON NOT NULL,
expire TIMESTAMP(6) NOT NULL
);
`;
const createPublicitiesTable = `
CREATE TABLE IF NOT EXISTS publicities (
id SERIAL PRIMARY KEY,
pub_number VARCHAR(255) NOT NULL,
designation TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
year INTEGER NOT NULL,
created_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createPublicityParticipationsTable = `
CREATE TABLE IF NOT EXISTS publicity_participations (
publicity_id INTEGER REFERENCES publicities(id) ON DELETE CASCADE,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (publicity_id, group_id)
);
`;
const createRolesTable = `
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
description TEXT,
color VARCHAR(7) DEFAULT '#6b7280',
is_system BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createPermissionsTable = `
CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(255) NOT NULL,
action VARCHAR(255) NOT NULL,
resource VARCHAR(255) NOT NULL,
is_system BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createRolePermissionsTable = `
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
`;
const createNocodbConfigTable = `
CREATE TABLE IF NOT EXISTS nocodb_config (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
base_url VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
api_token VARCHAR(255) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT true,
created_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createCustomerOrdersTable = `
CREATE TABLE IF NOT EXISTS customer_orders (
id SERIAL PRIMARY KEY,
order_taker VARCHAR(255) NOT NULL,
customer_name VARCHAR(255) NOT NULL,
customer_phone VARCHAR(255),
customer_email VARCHAR(255),
product_designation TEXT NOT NULL,
product_reference VARCHAR(255),
gencode VARCHAR(255),
quantity INTEGER DEFAULT 1,
supplier_id INTEGER REFERENCES suppliers(id) ON DELETE SET NULL,
status VARCHAR(100) DEFAULT 'En attente de Commande',
deposit DECIMAL(10,2) DEFAULT 0.00,
is_promotional_price BOOLEAN DEFAULT false,
customer_notified BOOLEAN DEFAULT false,
notes TEXT,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
created_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
const createDlcProductsTable = `
CREATE TABLE IF NOT EXISTS dlc_products (
id SERIAL PRIMARY KEY,
product_name VARCHAR(255) NOT NULL,
expiry_date DATE NOT NULL,
date_type VARCHAR(50) NOT NULL DEFAULT 'DLC',
quantity INTEGER NOT NULL DEFAULT 1,
unit VARCHAR(50) NOT NULL DEFAULT 'unité',
supplier_id INTEGER NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE,
location VARCHAR(255) NOT NULL DEFAULT 'Magasin',
status VARCHAR(50) NOT NULL DEFAULT 'active',
notes TEXT,
alert_threshold INTEGER NOT NULL DEFAULT 15,
validated_at TIMESTAMP,
validated_by VARCHAR(255) REFERENCES users(id),
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
created_by VARCHAR(255) NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
gencode VARCHAR(255)
);
`;
const createTasksTable = `
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
priority VARCHAR(50) NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
assigned_to VARCHAR(255),
due_date DATE,
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
created_by VARCHAR(255) NOT NULL REFERENCES users(id),
completed_at TIMESTAMP,
completed_by VARCHAR(255) REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`;
const tables = [
createUsersTable,
createGroupsTable,
createSuppliersTable,
createOrdersTable,
createDeliveriesTable,
createUserGroupsTable,
createSessionTable,
createPublicitiesTable,
createPublicityParticipationsTable,
createRolesTable,
createPermissionsTable,
createRolePermissionsTable,
createNocodbConfigTable,
createCustomerOrdersTable,
createDlcProductsTable,
createTasksTable
];
for (const table of tables) {
await pool.query(table);
}
console.log('✅ All tables verified/created successfully');
}
async function runMigrations() {
try {
console.log('🔄 Running database migrations...');
// Migration 1: Add missing columns to existing tables
await addMissingColumns();
// Migration 2: Update constraints
await updateConstraints();
// Migration 3: Create new tables for roles/permissions if they don't exist
await createRolesTables();
console.log('✅ All migrations completed successfully');
} catch (error) {
console.error('❌ Migration failed:', error);
// Don't throw error, continue with existing tables
}
}
async function addMissingColumns() {
try {
// Check and add delivered_date column to deliveries
const deliveredDateExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'deliveries' AND column_name = 'delivered_date'
`);
if (deliveredDateExists.rows.length === 0) {
await pool.query('ALTER TABLE deliveries ADD COLUMN delivered_date TIMESTAMP');
console.log('✅ Added delivered_date column to deliveries');
}
// Check and add validated_at column to deliveries
const validatedAtExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'deliveries' AND column_name = 'validated_at'
`);
if (validatedAtExists.rows.length === 0) {
await pool.query('ALTER TABLE deliveries ADD COLUMN validated_at TIMESTAMP');
console.log('✅ Added validated_at column to deliveries');
}
// CRITICAL FIX: Corriger la contrainte deliveries_status_check pour permettre 'planned'
console.log('🔧 CRITICAL FIX: Correcting deliveries status constraint...');
try {
await pool.query('ALTER TABLE deliveries DROP CONSTRAINT IF EXISTS deliveries_status_check');
await pool.query("ALTER TABLE deliveries ADD CONSTRAINT deliveries_status_check CHECK (status IN ('pending', 'planned', 'delivered', 'cancelled'))");
console.log('✅ CRITICAL FIX: Deliveries status constraint corrected to allow planned status');
} catch (error) {
console.error('❌ Failed to fix deliveries constraint:', error);
}
// Check and add MISSING columns to users table
const columnsToAdd = [
{ name: 'name', type: 'VARCHAR(255)', default: null },
{ name: 'first_name', type: 'VARCHAR(255)', default: null },
{ name: 'last_name', type: 'VARCHAR(255)', default: null },
{ name: 'profile_image_url', type: 'TEXT', default: null }
];
for (const column of columnsToAdd) {
const columnExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = $1
`, [column.name]);
if (columnExists.rows.length === 0) {
await pool.query(`ALTER TABLE users ADD COLUMN ${column.name} ${column.type}`);
console.log(`✅ Added ${column.name} column to users table`);
}
}
// Populate name column if empty
await pool.query(`
UPDATE users SET name = COALESCE(
CASE
WHEN first_name IS NOT NULL AND last_name IS NOT NULL THEN first_name || ' ' || last_name
WHEN first_name IS NOT NULL THEN first_name
WHEN last_name IS NOT NULL THEN last_name
ELSE username
END,
username,
email
)
WHERE name IS NULL OR name = ''
`);
console.log('✅ Updated name column with existing data');
// Check and add customer_email column to customer_orders
const customerEmailExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'customer_orders' AND column_name = 'customer_email'
`);
if (customerEmailExists.rows.length === 0) {
await pool.query('ALTER TABLE customer_orders ADD COLUMN customer_email VARCHAR(255)');
console.log('✅ Added customer_email column to customer_orders');
}
// Check and add notes column to customer_orders
const notesExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'customer_orders' AND column_name = 'notes'
`);
if (notesExists.rows.length === 0) {
await pool.query('ALTER TABLE customer_orders ADD COLUMN notes TEXT');
console.log('✅ Added notes column to customer_orders');
}
// Check and add quantity column to customer_orders
const quantityExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'customer_orders' AND column_name = 'quantity'
`);
if (quantityExists.rows.length === 0) {
await pool.query('ALTER TABLE customer_orders ADD COLUMN quantity INTEGER DEFAULT 1');
console.log('✅ Added quantity column to customer_orders');
}
// Check and add quantity column to orders
const ordersQuantityExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'orders' AND column_name = 'quantity'
`);
if (ordersQuantityExists.rows.length === 0) {
await pool.query('ALTER TABLE orders ADD COLUMN quantity INTEGER');
console.log('✅ Added quantity column to orders');
}
// Check and add unit column to orders
const ordersUnitExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'orders' AND column_name = 'unit'
`);
if (ordersUnitExists.rows.length === 0) {
await pool.query('ALTER TABLE orders ADD COLUMN unit VARCHAR(50)');
console.log('✅ Added unit column to orders');
}
// Check and add completed_by column to tasks
const tasksCompletedByExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'tasks' AND column_name = 'completed_by'
`);
if (tasksCompletedByExists.rows.length === 0) {
await pool.query('ALTER TABLE tasks ADD COLUMN completed_by VARCHAR(255) REFERENCES users(id)');
console.log('✅ Added completed_by column to tasks');
}
// FORCE FIX PRODUCTION ISSUE: Recreate tasks table completed columns (Critical Fix v2)
console.log('🔧 CRITICAL FIX v2: Forcing tasks table completed columns for production...');
try {
// First verify current structure
const currentCols = await pool.query(`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'tasks' AND column_name IN ('completed_at', 'completed_by')
`);
console.log('📋 Current completed columns:', currentCols.rows);
// Drop existing constraints first
await pool.query(`ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_completed_by_fkey CASCADE;`);
console.log('✅ Dropped constraints');
// Force drop and recreate columns
await pool.query(`ALTER TABLE tasks DROP COLUMN IF EXISTS completed_at CASCADE;`);
await pool.query(`ALTER TABLE tasks DROP COLUMN IF EXISTS completed_by CASCADE;`);
console.log('✅ Dropped old columns');
await pool.query(`ALTER TABLE tasks ADD COLUMN completed_at TIMESTAMP NULL;`);
await pool.query(`ALTER TABLE tasks ADD COLUMN completed_by VARCHAR(255) NULL;`);
console.log('✅ Added new columns');
// Re-add foreign key constraint
await pool.query(`
ALTER TABLE tasks ADD CONSTRAINT tasks_completed_by_fkey
FOREIGN KEY (completed_by) REFERENCES users(id) ON DELETE SET NULL;
`);
console.log('✅ Added constraints');
// Final verification
const verifyResult = await pool.query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'tasks' AND column_name IN ('completed_at', 'completed_by')
ORDER BY column_name
`);
console.log('🎉 CRITICAL FIX v2 COMPLETED. Final structure:', verifyResult.rows);
} catch (taskError) {
console.error('❌ Critical fix failed:', taskError);
throw taskError; // Force error to prevent app from starting with broken schema
}
// Add missing columns to roles table
const rolesColumnsToAdd = [
{ name: 'display_name', type: 'VARCHAR(255)', default: null },
{ name: 'color', type: 'VARCHAR(7)', default: "'#6b7280'" },
{ name: 'is_active', type: 'BOOLEAN', default: 'true' }
];
for (const column of rolesColumnsToAdd) {
const columnExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = $1
`, [column.name]);
if (columnExists.rows.length === 0) {
const defaultClause = column.default ? ` DEFAULT ${column.default}` : '';
await pool.query(`ALTER TABLE roles ADD COLUMN ${column.name} ${column.type}${defaultClause}`);
console.log(`✅ Added ${column.name} column to roles table`);
// Migrate display_name from name if needed
if (column.name === 'display_name') {
await pool.query(`UPDATE roles SET display_name = name WHERE display_name IS NULL`);
console.log('✅ Migrated display_name data from name column');
}
}
}
// Add missing columns to permissions table
const permissionsColumnsToAdd = [
{ name: 'display_name', type: 'VARCHAR(255)', default: null },
{ name: 'action', type: 'VARCHAR(255)', default: "'read'" },
{ name: 'resource', type: 'VARCHAR(255)', default: "'system'" },
{ name: 'is_system', type: 'BOOLEAN', default: 'true' }
];
for (const column of permissionsColumnsToAdd) {
const columnExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'permissions' AND column_name = $1
`, [column.name]);
if (columnExists.rows.length === 0) {
const defaultClause = column.default ? ` DEFAULT ${column.default}` : '';
await pool.query(`ALTER TABLE permissions ADD COLUMN ${column.name} ${column.type}${defaultClause}`);
console.log(`✅ Added ${column.name} column to permissions table`);
// Migrate display_name from name if needed
if (column.name === 'display_name') {
await pool.query(`UPDATE permissions SET display_name = name WHERE display_name IS NULL`);
console.log('✅ Migrated permissions display_name data');
}
}
}
// Add description column to nocodb_config table
const descriptionExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'nocodb_config' AND column_name = 'description'
`);
if (descriptionExists.rows.length === 0) {
await pool.query('ALTER TABLE nocodb_config ADD COLUMN description TEXT');
console.log('✅ Added description column to nocodb_config');
}
// Add has_dlc column to suppliers table
const supplierHasDlcExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'suppliers' AND column_name = 'has_dlc'
`);
if (supplierHasDlcExists.rows.length === 0) {
await pool.query('ALTER TABLE suppliers ADD COLUMN has_dlc BOOLEAN DEFAULT FALSE');
console.log('✅ Added has_dlc column to suppliers table');
}
// Add missing columns to dlc_products table for compatibility
const dlcColumnsToAdd = [
{ name: 'name', type: 'VARCHAR(255)', default: null },
{ name: 'dlc_date', type: 'DATE', default: null },
{ name: 'product_code', type: 'VARCHAR(255)', default: null },
{ name: 'description', type: 'TEXT', default: null }
];
for (const column of dlcColumnsToAdd) {
const columnExists = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'dlc_products' AND column_name = $1
`, [column.name]);
if (columnExists.rows.length === 0) {
const defaultClause = column.default ? ` DEFAULT ${column.default}` : '';
await pool.query(`ALTER TABLE dlc_products ADD COLUMN ${column.name} ${column.type}${defaultClause}`);
console.log(`✅ Added ${column.name} column to dlc_products table`);
}
}
// Migrate existing data if needed
const needsMigration = await pool.query(`
SELECT COUNT(*) as count FROM dlc_products
WHERE name IS NULL AND product_name IS NOT NULL
`);
if (parseInt(needsMigration.rows[0].count) > 0) {
await pool.query(`
UPDATE dlc_products SET
name = product_name,
dlc_date = expiry_date
WHERE name IS NULL OR dlc_date IS NULL
`);
console.log('✅ Migrated DLC product data to new columns');
}
} catch (error) {
console.error('❌ Error adding missing columns:', error);
}
}
async function updateConstraints() {
try {
// Check if orders_status_check constraint exists and needs updating
const constraintExists = await pool.query(`
SELECT 1 FROM information_schema.check_constraints
WHERE constraint_name = 'orders_status_check'
`);
if (constraintExists.rows.length > 0) {
// Drop old constraint
await pool.query('ALTER TABLE orders DROP CONSTRAINT orders_status_check');
console.log('✅ Removed old orders_status_check constraint');
}
// Add updated constraint
await pool.query(`
ALTER TABLE orders ADD CONSTRAINT orders_status_check
CHECK (status IN ('pending', 'planned', 'delivered'))
`);
console.log('✅ Added updated orders_status_check constraint');
} catch (error) {
console.error('❌ Error updating constraints:', error);
}
}
async function createRolesTables() {
try {
// Create roles table avec colonnes manquantes
await pool.query(`
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255),
description TEXT,
color VARCHAR(20) DEFAULT '#6b7280',
is_system BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create permissions table avec colonnes manquantes
await pool.query(`
CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255),
description TEXT,
category VARCHAR(255),
action VARCHAR(255),
resource VARCHAR(255),
is_system BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create role_permissions table
await pool.query(`
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_id, permission_id)
)
`);
// Create USER_ROLES table (CORRECTION CRITIQUE - cette table manquait !)
await pool.query(`
CREATE TABLE IF NOT EXISTS user_roles (
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
assigned_by VARCHAR NOT NULL REFERENCES users(id),
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
)
`);
// Create indexes for performance sur user_roles
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles (user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles (role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_assigned_by ON user_roles (assigned_by);
`);
// Ajouter les colonnes manquantes aux tables existantes si elles n'existent pas
const addColumnsQueries = [
`ALTER TABLE roles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255);`,
`ALTER TABLE roles ADD COLUMN IF NOT EXISTS color VARCHAR(20) DEFAULT '#6b7280';`,
`ALTER TABLE roles ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;`,
`ALTER TABLE permissions ADD COLUMN IF NOT EXISTS display_name VARCHAR(255);`,
`ALTER TABLE permissions ADD COLUMN IF NOT EXISTS action VARCHAR(255);`,
`ALTER TABLE permissions ADD COLUMN IF NOT EXISTS resource VARCHAR(255);`,
`ALTER TABLE permissions ADD COLUMN IF NOT EXISTS is_system BOOLEAN DEFAULT false;`
];
for (const query of addColumnsQueries) {
try {
await pool.query(query);
} catch (err) {
// Ignorer erreurs si colonnes existent déjà
console.log('Column already exists or minor error:', err.message.substring(0, 50));
}
}
// Vérifier et assigner rôle admin à admin_local si nécessaire
const adminRoleCheck = await pool.query(`
SELECT COUNT(*) as count FROM user_roles
WHERE user_id = 'admin_local'
`);
if (adminRoleCheck.rows[0].count === '0') {
// CORRECTION CRITIQUE: utiliser 'admin_local' au lieu de 'system' pour assigned_by
await pool.query(`
INSERT INTO user_roles (user_id, role_id, assigned_by, assigned_at)
SELECT 'admin_local', r.id, 'admin_local', CURRENT_TIMESTAMP
FROM roles r
WHERE r.name = 'admin'
AND EXISTS (SELECT 1 FROM users WHERE id = 'admin_local')
`);
console.log('✅ Admin role assigned to admin_local user (self-assigned)');
}
console.log('✅ ALL tables created including CRITICAL user_roles table');
} catch (error) {
console.error('❌ Error creating roles tables:', error);
// Continue anyway, don't crash the app
}
}
async function createDefaultAdmin() {
try {
// Check if admin user exists
const existingAdmin = await pool.query(
'SELECT id, password_changed FROM users WHERE username = $1',
['admin']
);
if (existingAdmin.rows.length === 0) {
// Import hash function without bcrypt
const { hashPassword } = await import('./auth-utils.production');
const hashedPassword = await hashPassword('admin');
await pool.query(`
INSERT INTO users (id, username, email, name, first_name, last_name, password, role, password_changed)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`, [
'admin_local',
'admin',
'admin@logiflow.com',
'Administrateur',
'Admin',
'LogiFlow',
hashedPassword,
'admin',
false
]);
console.log('✅ Default admin user created: admin/admin');
} else {
// Only reset password if it hasn't been changed by the user
const admin = existingAdmin.rows[0];
if (!admin.password_changed) {
const { hashPassword } = await import('./auth-utils.production');
const newHashedPassword = await hashPassword('admin');
await pool.query(
'UPDATE users SET password = $1 WHERE username = $2',
[newHashedPassword, 'admin']
);
console.log('✅ Admin user password updated with default password (admin/admin)');
} else {
console.log('✅ Admin user exists with custom password - not resetting');
}
}
} catch (error) {
console.error('❌ Failed to create admin user:', error);
}
}
async function initRolesAndPermissionsProduction() {
try {
console.log('🎭 Initializing roles and permissions for production...');
// Check if roles already exist (avoid re-creating)
const existingRoles = await pool.query('SELECT COUNT(*) as count FROM roles');
if (existingRoles.rows[0].count > 0) {
console.log('✅ Roles already exist, checking for missing DLC permissions...');
await ensureDlcPermissionsExist();
return;
}
// Create 4 default roles with proper French names
const roles = [
{ name: 'admin', displayName: 'Administrateur', description: 'Accès complet à toutes les fonctionnalités', color: '#dc2626' },
{ name: 'manager', displayName: 'Manager', description: 'Gestion des commandes, livraisons et fournisseurs', color: '#2563eb' },
{ name: 'employee', displayName: 'Employé', description: 'Accès en lecture aux données et publicités', color: '#16a34a' },
{ name: 'directeur', displayName: 'Directeur', description: 'Supervision générale et gestion stratégique', color: '#7c3aed' }
];
const createdRoles = {};
for (const role of roles) {
const result = await pool.query(`
INSERT INTO roles (name, display_name, description, color, is_system, is_active)
VALUES ($1, $2, $3, $4, true, true)
RETURNING id, name
`, [role.name, role.displayName, role.description, role.color]);
createdRoles[role.name] = result.rows[0].id;
console.log(`✅ Created role: ${role.displayName}`);
}
// Create 49 permissions with French categories
const permissions = [
// Dashboard (tableau_de_bord)
{ category: 'tableau_de_bord', name: 'dashboard_read', displayName: 'Voir tableau de bord', description: 'Accès en lecture au tableau de bord', action: 'read', resource: 'dashboard' },
// Stores (magasins)
{ category: 'magasins', name: 'groups_read', displayName: 'Voir magasins', description: 'Accès en lecture aux magasins', action: 'read', resource: 'groups' },
{ category: 'magasins', name: 'groups_create', displayName: 'Créer magasins', description: 'Création de nouveaux magasins', action: 'create', resource: 'groups' },
{ category: 'magasins', name: 'groups_update', displayName: 'Modifier magasins', description: 'Modification des magasins existants', action: 'update', resource: 'groups' },
{ category: 'magasins', name: 'groups_delete', displayName: 'Supprimer magasins', description: 'Suppression de magasins', action: 'delete', resource: 'groups' },
// Suppliers (fournisseurs)
{ category: 'fournisseurs', name: 'suppliers_read', displayName: 'Voir fournisseurs', description: 'Accès en lecture aux fournisseurs', action: 'read', resource: 'suppliers' },
{ category: 'fournisseurs', name: 'suppliers_create', displayName: 'Créer fournisseurs', description: 'Création de nouveaux fournisseurs', action: 'create', resource: 'suppliers' },
{ category: 'fournisseurs', name: 'suppliers_update', displayName: 'Modifier fournisseurs', description: 'Modification des fournisseurs', action: 'update', resource: 'suppliers' },
{ category: 'fournisseurs', name: 'suppliers_delete', displayName: 'Supprimer fournisseurs', description: 'Suppression de fournisseurs', action: 'delete', resource: 'suppliers' },
// Orders (commandes)
{ category: 'commandes', name: 'orders_read', displayName: 'Voir commandes', description: 'Accès en lecture aux commandes', action: 'read', resource: 'orders' },
{ category: 'commandes', name: 'orders_create', displayName: 'Créer commandes', description: 'Création de nouvelles commandes', action: 'create', resource: 'orders' },
{ category: 'commandes', name: 'orders_update', displayName: 'Modifier commandes', description: 'Modification des commandes', action: 'update', resource: 'orders' },
{ category: 'commandes', name: 'orders_delete', displayName: 'Supprimer commandes', description: 'Suppression de commandes', action: 'delete', resource: 'orders' },
// Deliveries (livraisons)
{ category: 'livraisons', name: 'deliveries_read', displayName: 'Voir livraisons', description: 'Accès en lecture aux livraisons', action: 'read', resource: 'deliveries' },
{ category: 'livraisons', name: 'deliveries_create', displayName: 'Créer livraisons', description: 'Création de nouvelles livraisons', action: 'create', resource: 'deliveries' },
{ category: 'livraisons', name: 'deliveries_update', displayName: 'Modifier livraisons', description: 'Modification des livraisons', action: 'update', resource: 'deliveries' },
{ category: 'livraisons', name: 'deliveries_delete', displayName: 'Supprimer livraisons', description: 'Suppression de livraisons', action: 'delete', resource: 'deliveries' },
// Publicities (publicites)
{ category: 'publicites', name: 'publicities_read', displayName: 'Voir publicités', description: 'Accès en lecture aux publicités', action: 'read', resource: 'publicities' },
{ category: 'publicites', name: 'publicities_create', displayName: 'Créer publicités', description: 'Création de nouvelles publicités', action: 'create', resource: 'publicities' },
{ category: 'publicites', name: 'publicities_update', displayName: 'Modifier publicités', description: 'Modification des publicités', action: 'update', resource: 'publicities' },
{ category: 'publicites', name: 'publicities_delete', displayName: 'Supprimer publicités', description: 'Suppression de publicités', action: 'delete', resource: 'publicities' },
{ category: 'publicites', name: 'publicities_participate', displayName: 'Participer aux publicités', description: 'Participation des magasins aux publicités', action: 'participate', resource: 'publicities' },
// Customer Orders (commandes_clients)
{ category: 'commandes_clients', name: 'customer_orders_read', displayName: 'Voir commandes clients', description: 'Accès en lecture aux commandes clients', action: 'read', resource: 'customer_orders' },
{ category: 'commandes_clients', name: 'customer_orders_create', displayName: 'Créer commandes clients', description: 'Création de nouvelles commandes clients', action: 'create', resource: 'customer_orders' },
{ category: 'commandes_clients', name: 'customer_orders_update', displayName: 'Modifier commandes clients', description: 'Modification des commandes clients', action: 'update', resource: 'customer_orders' },
{ category: 'commandes_clients', name: 'customer_orders_delete', displayName: 'Supprimer commandes clients', description: 'Suppression de commandes clients', action: 'delete', resource: 'customer_orders' },
{ category: 'commandes_clients', name: 'customer_orders_print', displayName: 'Imprimer commandes clients', description: 'Impression des barcodes et documents', action: 'print', resource: 'customer_orders' },
// Users (utilisateurs)
{ category: 'utilisateurs', name: 'users_read', displayName: 'Voir utilisateurs', description: 'Accès en lecture aux utilisateurs', action: 'read', resource: 'users' },
{ category: 'utilisateurs', name: 'users_create', displayName: 'Créer utilisateurs', description: 'Création de nouveaux utilisateurs', action: 'create', resource: 'users' },
{ category: 'utilisateurs', name: 'users_update', displayName: 'Modifier utilisateurs', description: 'Modification des utilisateurs', action: 'update', resource: 'users' },
{ category: 'utilisateurs', name: 'users_delete', displayName: 'Supprimer utilisateurs', description: 'Suppression d\'utilisateurs', action: 'delete', resource: 'users' },
// Role Management (gestion_roles)
{ category: 'gestion_roles', name: 'roles_read', displayName: 'Voir rôles', description: 'Accès en lecture aux rôles', action: 'read', resource: 'roles' },
{ category: 'gestion_roles', name: 'roles_create', displayName: 'Créer rôles', description: 'Création de nouveaux rôles', action: 'create', resource: 'roles' },
{ category: 'gestion_roles', name: 'roles_update', displayName: 'Modifier rôles', description: 'Modification des rôles', action: 'update', resource: 'roles' },
{ category: 'gestion_roles', name: 'roles_delete', displayName: 'Supprimer rôles', description: 'Suppression de rôles', action: 'delete', resource: 'roles' },
{ category: 'gestion_roles', name: 'permissions_read', displayName: 'Voir permissions', description: 'Accès en lecture aux permissions', action: 'read', resource: 'permissions' },
{ category: 'gestion_roles', name: 'permissions_assign', displayName: 'Assigner permissions', description: 'Attribution de permissions aux rôles', action: 'assign', resource: 'permissions' },
// Reconciliation (rapprochement)
{ category: 'rapprochement', name: 'bl_reconciliation_read', displayName: 'Voir rapprochement BL', description: 'Accès au rapprochement des bons de livraison', action: 'read', resource: 'bl_reconciliation' },
{ category: 'rapprochement', name: 'bl_reconciliation_update', displayName: 'Modifier rapprochement BL', description: 'Modification du rapprochement des BL', action: 'update', resource: 'bl_reconciliation' },
// Administration
{ category: 'administration', name: 'nocodb_config_read', displayName: 'Voir config NocoDB', description: 'Accès à la configuration NocoDB', action: 'read', resource: 'nocodb_config' },
{ category: 'administration', name: 'nocodb_config_update', displayName: 'Modifier config NocoDB', description: 'Modification de la configuration NocoDB', action: 'update', resource: 'nocodb_config' },
{ category: 'administration', name: 'system_admin', displayName: 'Administration système', description: 'Accès complet à l\'administration', action: 'admin', resource: 'system' },
// Calendar (calendrier)
{ category: 'calendrier', name: 'calendar_read', displayName: 'Voir calendrier', description: 'Accès en lecture au calendrier', action: 'read', resource: 'calendar' },
{ category: 'calendrier', name: 'calendar_update', displayName: 'Modifier calendrier', description: 'Modification des événements du calendrier', action: 'update', resource: 'calendar' },
// DLC Management (gestion_dlc)
{ category: 'gestion_dlc', name: 'dlc_read', displayName: 'Voir produits DLC', description: 'Accès en lecture aux produits DLC', action: 'read', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_create', displayName: 'Créer produits DLC', description: 'Création de nouveaux produits DLC', action: 'create', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_update', displayName: 'Modifier produits DLC', description: 'Modification des produits DLC', action: 'update', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_delete', displayName: 'Supprimer produits DLC', description: 'Suppression de produits DLC', action: 'delete', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_validate', displayName: 'Valider produits DLC', description: 'Validation des produits DLC', action: 'validate', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_print', displayName: 'Imprimer étiquettes DLC', description: 'Impression des étiquettes DLC', action: 'print', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_stats', displayName: 'Voir statistiques DLC', description: 'Accès aux statistiques DLC', action: 'stats', resource: 'dlc' },
// Task Management (gestion_taches)
{ category: 'gestion_taches', name: 'tasks_read', displayName: 'Voir tâches', description: 'Accès en lecture aux tâches', action: 'read', resource: 'tasks' },
{ category: 'gestion_taches', name: 'tasks_create', displayName: 'Créer tâches', description: 'Création de nouvelles tâches', action: 'create', resource: 'tasks' },
{ category: 'gestion_taches', name: 'tasks_update', displayName: 'Modifier tâches', description: 'Modification des tâches', action: 'update', resource: 'tasks' },
{ category: 'gestion_taches', name: 'tasks_delete', displayName: 'Supprimer tâches', description: 'Suppression de tâches', action: 'delete', resource: 'tasks' },
{ category: 'gestion_taches', name: 'tasks_assign', displayName: 'Assigner tâches', description: 'Attribution de tâches aux utilisateurs', action: 'assign', resource: 'tasks' },
{ category: 'gestion_dlc', name: 'dlc_delete', displayName: 'Supprimer produits DLC', description: 'Suppression de produits DLC', action: 'delete', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_validate', displayName: 'Valider produits DLC', description: 'Validation des produits DLC', action: 'validate', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_print', displayName: 'Imprimer étiquettes DLC', description: 'Impression des étiquettes DLC', action: 'print', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_stats', displayName: 'Voir statistiques DLC', description: 'Accès aux statistiques DLC', action: 'read', resource: 'dlc_stats' }
];
const createdPermissions = {};
for (const perm of permissions) {
const result = await pool.query(`
INSERT INTO permissions (name, display_name, description, category, action, resource, is_system)
VALUES ($1, $2, $3, $4, $5, $6, true)
RETURNING id, name
`, [perm.name, perm.displayName, perm.description, perm.category, perm.action, perm.resource]);
createdPermissions[perm.name] = result.rows[0].id;
}
console.log(`✅ Created ${permissions.length} permissions`);
// Assign permissions to roles
const rolePermissions = {
'admin': Object.keys(createdPermissions), // Admin gets all permissions
'manager': [
'dashboard_read', 'groups_read', 'suppliers_read', 'suppliers_create', 'suppliers_update',
'orders_read', 'orders_create', 'orders_update', 'deliveries_read', 'deliveries_create',
'deliveries_update', 'publicities_read', 'publicities_create', 'publicities_update',
'publicities_participate', 'customer_orders_read', 'customer_orders_create',
'customer_orders_update', 'customer_orders_print', 'users_read', 'bl_reconciliation_read',
'bl_reconciliation_update', 'calendar_read', 'calendar_update',
'dlc_read', 'dlc_create', 'dlc_update', 'dlc_validate', 'dlc_print', 'dlc_stats',
'tasks_read', 'tasks_create', 'tasks_update', 'tasks_assign'
],
'employee': [
'dashboard_read', 'groups_read', 'suppliers_read', 'orders_read', 'deliveries_read',
'publicities_read', 'customer_orders_read', 'customer_orders_create', 'customer_orders_print',
'calendar_read', 'dlc_read', 'dlc_create', 'dlc_update',
'tasks_read', 'tasks_create', 'tasks_update'
],
'directeur': [
'dashboard_read', 'groups_read', 'groups_create', 'groups_update', 'suppliers_read',
'suppliers_create', 'suppliers_update', 'orders_read', 'orders_create', 'orders_update',
'deliveries_read', 'deliveries_create', 'deliveries_update', 'publicities_read',
'publicities_create', 'publicities_update', 'publicities_participate',
'customer_orders_read', 'customer_orders_create', 'customer_orders_update',
'customer_orders_print', 'users_read', 'users_create', 'users_update', 'roles_read',
'bl_reconciliation_read', 'bl_reconciliation_update', 'calendar_read', 'calendar_update',
'dlc_read', 'dlc_create', 'dlc_update', 'dlc_delete', 'dlc_validate', 'dlc_print', 'dlc_stats',
'tasks_read', 'tasks_create', 'tasks_update', 'tasks_delete', 'tasks_assign'
]
};
for (const [roleName, permissionNames] of Object.entries(rolePermissions)) {
const roleId = createdRoles[roleName];
for (const permName of permissionNames) {
const permId = createdPermissions[permName];
if (permId) {
await pool.query(`
INSERT INTO role_permissions (role_id, permission_id)
VALUES ($1, $2)
ON CONFLICT (role_id, permission_id) DO NOTHING
`, [roleId, permId]);
}
}
console.log(`✅ Assigned ${permissionNames.length} permissions to ${roleName}`);
}
console.log('✅ Production roles and permissions initialization complete!');
} catch (error) {
console.error('❌ Error initializing roles and permissions:', error);
// Continue anyway
}
}
async function ensureDlcPermissionsExist() {
try {
console.log('🔍 Checking for missing DLC permissions...');
// Check if DLC permissions exist
const dlcPermissions = await pool.query(`
SELECT name FROM permissions WHERE category = 'gestion_dlc'
`);
if (dlcPermissions.rows.length >= 7) {
console.log('✅ All DLC permissions already exist');
return;
}
console.log('⚠️ Missing DLC permissions, creating them...');
// Create missing DLC permissions
const requiredDlcPermissions = [
{ category: 'gestion_dlc', name: 'dlc_read', displayName: 'Voir produits DLC', description: 'Accès en lecture aux produits DLC', action: 'read', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_create', displayName: 'Créer produits DLC', description: 'Création de nouveaux produits DLC', action: 'create', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_update', displayName: 'Modifier produits DLC', description: 'Modification des produits DLC', action: 'update', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_delete', displayName: 'Supprimer produits DLC', description: 'Suppression de produits DLC', action: 'delete', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_validate', displayName: 'Valider produits DLC', description: 'Validation des produits DLC', action: 'validate', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_print', displayName: 'Imprimer étiquettes DLC', description: 'Impression des étiquettes DLC', action: 'print', resource: 'dlc' },
{ category: 'gestion_dlc', name: 'dlc_stats', displayName: 'Voir statistiques DLC', description: 'Accès aux statistiques DLC', action: 'stats', resource: 'dlc' }
];
const createdDlcPermissions = {};
for (const perm of requiredDlcPermissions) {
// Check if permission already exists
const existing = await pool.query('SELECT id FROM permissions WHERE name = $1', [perm.name]);
if (existing.rows.length === 0) {
const result = await pool.query(`
INSERT INTO permissions (name, display_name, description, category, action, resource, is_system)
VALUES ($1, $2, $3, $4, $5, $6, true)
RETURNING id, name
`, [perm.name, perm.displayName, perm.description, perm.category, perm.action, perm.resource]);
createdDlcPermissions[perm.name] = result.rows[0].id;
console.log(`✅ Created DLC permission: ${perm.displayName}`);
} else {
createdDlcPermissions[perm.name] = existing.rows[0].id;
console.log(`✅ DLC permission exists: ${perm.displayName}`);
}
}
// Assign DLC permissions to existing roles
const rolePermissions = {
'admin': ['dlc_read', 'dlc_create', 'dlc_update', 'dlc_delete', 'dlc_validate', 'dlc_print', 'dlc_stats'],
'manager': ['dlc_read', 'dlc_create', 'dlc_update', 'dlc_validate', 'dlc_print', 'dlc_stats'],
'employee': ['dlc_read', 'dlc_create', 'dlc_update'],
'directeur': ['dlc_read', 'dlc_create', 'dlc_update', 'dlc_delete', 'dlc_validate', 'dlc_print', 'dlc_stats']
};
for (const [roleName, permissionNames] of Object.entries(rolePermissions)) {
const roleQuery = await pool.query('SELECT id FROM roles WHERE name = $1', [roleName]);
if (roleQuery.rows.length > 0) {
const roleId = roleQuery.rows[0].id;
for (const permName of permissionNames) {
const permId = createdDlcPermissions[permName];
if (permId) {
await pool.query(`
INSERT INTO role_permissions (role_id, permission_id)
VALUES ($1, $2)
ON CONFLICT (role_id, permission_id) DO NOTHING
`, [roleId, permId]);
}
}
console.log(`✅ Assigned DLC permissions to ${roleName}`);
}
}
console.log('✅ DLC permissions initialization complete!');
} catch (error) {
console.error('❌ Error ensuring DLC permissions exist:', error);
// Continue anyway
}
}
export { pool };