const mysql = require('mysql2/promise'); const { Pool } = require('pg'); const bcrypt = require('bcryptjs'); require('dotenv').config(); const DB_CLIENT = process.env.DB_CLIENT || 'mysql'; // 'mysql' or 'postgres' // DB Configuration const dbConfig = { host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASS || '', database: process.env.DB_NAME || 'condominio', port: process.env.DB_PORT || (DB_CLIENT === 'postgres' ? 5432 : 3306), }; let mysqlPool = null; let pgPool = null; if (DB_CLIENT === 'postgres') { pgPool = new Pool(dbConfig); } else { mysqlPool = mysql.createPool({ ...dbConfig, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); } const executeQuery = async (sql, params = []) => { if (DB_CLIENT === 'postgres') { let paramIndex = 1; const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`); const result = await pgPool.query(pgSql, params); return [result.rows, result.fields]; } else { return await mysqlPool.query(sql, params); } }; const dbInterface = { query: executeQuery, getConnection: async () => { if (DB_CLIENT === 'postgres') { return { query: executeQuery, release: () => {} }; } else { return await mysqlPool.getConnection(); } } }; const initDb = async () => { try { const connection = await dbInterface.getConnection(); console.log(`Database connected successfully using ${DB_CLIENT}.`); const TIMESTAMP_TYPE = 'TIMESTAMP'; // 0. Settings Table (Global App Settings) await connection.query(` CREATE TABLE IF NOT EXISTS settings ( id INT PRIMARY KEY, current_year INT, smtp_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'} ) `); // 1. Condos Table await connection.query(` CREATE TABLE IF NOT EXISTS condos ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, address VARCHAR(255), street_number VARCHAR(20), city VARCHAR(100), province VARCHAR(100), zip_code VARCHAR(20), notes TEXT, iban VARCHAR(50), default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00, image VARCHAR(255), created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ) `); // Migration for condos: Add new address fields try { let hasCity = false; if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'"); hasCity = cols.some(c => c.column_name === 'city'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM condos"); hasCity = cols.some(c => c.Field === 'city'); } if (!hasCity) { console.log('Migrating: Adding address fields to condos...'); await connection.query("ALTER TABLE condos ADD COLUMN street_number VARCHAR(20)"); await connection.query("ALTER TABLE condos ADD COLUMN city VARCHAR(100)"); await connection.query("ALTER TABLE condos ADD COLUMN province VARCHAR(100)"); await connection.query("ALTER TABLE condos ADD COLUMN zip_code VARCHAR(20)"); await connection.query("ALTER TABLE condos ADD COLUMN notes TEXT"); } } catch(e) { console.warn("Condos migration warning:", e.message); } // 2. Families Table await connection.query(` CREATE TABLE IF NOT EXISTS families ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36), name VARCHAR(255) NOT NULL, unit_number VARCHAR(50), stair VARCHAR(50), floor VARCHAR(50), notes TEXT, contact_email VARCHAR(255), custom_monthly_quota DECIMAL(10, 2) NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // Migration for families: Add condo_id, custom_monthly_quota, stair, floor, notes try { let hasCondoId = false; let hasQuota = false; let hasStair = false; if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='families'"); hasCondoId = cols.some(c => c.column_name === 'condo_id'); hasQuota = cols.some(c => c.column_name === 'custom_monthly_quota'); hasStair = cols.some(c => c.column_name === 'stair'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM families"); hasCondoId = cols.some(c => c.Field === 'condo_id'); hasQuota = cols.some(c => c.Field === 'custom_monthly_quota'); hasStair = cols.some(c => c.Field === 'stair'); } if (!hasCondoId) { console.log('Migrating: Adding condo_id to families...'); await connection.query("ALTER TABLE families ADD COLUMN condo_id VARCHAR(36)"); } if (!hasQuota) { console.log('Migrating: Adding custom_monthly_quota to families...'); await connection.query("ALTER TABLE families ADD COLUMN custom_monthly_quota DECIMAL(10, 2) NULL"); } if (!hasStair) { console.log('Migrating: Adding extended fields to families...'); await connection.query("ALTER TABLE families ADD COLUMN stair VARCHAR(50)"); await connection.query("ALTER TABLE families ADD COLUMN floor VARCHAR(50)"); await connection.query("ALTER TABLE families ADD COLUMN notes TEXT"); } } catch(e) { console.warn("Families migration warning:", e.message); } // 3. Payments Table await connection.query(` CREATE TABLE IF NOT EXISTS payments ( id VARCHAR(36) PRIMARY KEY, family_id VARCHAR(36) NOT NULL, amount DECIMAL(10, 2) NOT NULL, date_paid ${TIMESTAMP_TYPE} NOT NULL, for_month INT NOT NULL, for_year INT NOT NULL, notes TEXT, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE ) `); // 4. Users Table await connection.query(` CREATE TABLE IF NOT EXISTS users ( id VARCHAR(36) PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255), role VARCHAR(20) DEFAULT 'user', phone VARCHAR(20), family_id VARCHAR(36) NULL, receive_alerts BOOLEAN DEFAULT TRUE, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE SET NULL ) `); // 5. Alerts Table await connection.query(` CREATE TABLE IF NOT EXISTS alerts ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NULL, subject VARCHAR(255) NOT NULL, body TEXT, days_offset INT DEFAULT 1, offset_type VARCHAR(50) DEFAULT 'before_next_month', send_hour INT DEFAULT 9, active BOOLEAN DEFAULT TRUE, last_sent ${TIMESTAMP_TYPE} NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // Migration for alerts: Add condo_id if missing try { let hasCondoId = false; if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='alerts'"); hasCondoId = cols.some(c => c.column_name === 'condo_id'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM alerts"); hasCondoId = cols.some(c => c.Field === 'condo_id'); } if (!hasCondoId) { console.log('Migrating: Adding condo_id to alerts...'); await connection.query("ALTER TABLE alerts ADD COLUMN condo_id VARCHAR(36)"); await connection.query("ALTER TABLE alerts ADD CONSTRAINT fk_alerts_condo FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE"); } } catch(e) { console.warn("Alerts migration warning:", e.message); } // 6. Notices Table await connection.query(` CREATE TABLE IF NOT EXISTS notices ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, content TEXT, type VARCHAR(50) DEFAULT 'info', link VARCHAR(255), date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, active BOOLEAN DEFAULT TRUE, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // 7. Notice Reads await connection.query(` CREATE TABLE IF NOT EXISTS notice_reads ( user_id VARCHAR(36), notice_id VARCHAR(36), read_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, notice_id), FOREIGN KEY (notice_id) REFERENCES notices(id) ON DELETE CASCADE ) `); // --- SEEDING --- const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1'); if (rows.length === 0) { const currentYear = new Date().getFullYear(); await connection.query( 'INSERT INTO settings (id, current_year) VALUES (1, ?)', [currentYear] ); } // ENSURE ADMIN EXISTS AND HAS CORRECT ROLE const [admins] = await connection.query('SELECT * FROM users WHERE email = ?', ['fcarra79@gmail.com']); if (admins.length === 0) { const hashedPassword = await bcrypt.hash('Mr10921.', 10); const { v4: uuidv4 } = require('uuid'); await connection.query( 'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)', [uuidv4(), 'fcarra79@gmail.com', hashedPassword, 'Amministratore', 'admin'] ); console.log('Default Admin user created.'); } else { // Force update role to admin just in case it was changed or created wrongly await connection.query('UPDATE users SET role = ? WHERE email = ?', ['admin', 'fcarra79@gmail.com']); console.log('Ensured default user has admin role.'); } console.log('Database tables initialized.'); if (connection.release) connection.release(); } catch (error) { console.error('Database initialization failed:', error); process.exit(1); } }; module.exports = { pool: dbInterface, initDb };