Files
Condopay/server/db.js
frakarr 26fc451871 feat: Add email configuration and alert system
Introduces SMTP configuration settings and alert definitions to enable automated email notifications.
This includes new types for `SmtpConfig` and `AlertDefinition`, and integrates these into the settings page and mock database.
Adds styling for select elements and scrollbar hiding in the main HTML.
Updates mock database logic to potentially support local development without a backend.
2025-12-06 23:01:02 +01:00

217 lines
7.6 KiB
JavaScript

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
});
}
// Wrapper to normalize query execution between MySQL and Postgres
// MySQL uses '?' for placeholders, Postgres uses '$1', '$2', etc.
// MySQL returns [rows, fields], pg returns result object with .rows
const executeQuery = async (sql, params = []) => {
if (DB_CLIENT === 'postgres') {
// Convert ? to $1, $2, ...
let paramIndex = 1;
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
const result = await pgPool.query(pgSql, params);
// Normalize return to match mysql2 [rows, fields] signature
return [result.rows, result.fields];
} else {
return await mysqlPool.query(sql, params);
}
};
// Interface object to be used by server.js
const dbInterface = {
query: executeQuery,
getConnection: async () => {
if (DB_CLIENT === 'postgres') {
// Postgres pool handles connections automatically, but we provide a mock
// release function to satisfy the existing initDb pattern if needed.
// For general queries, we use the pool directly in executeQuery.
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}.`);
// Helper for syntax differences
const AUTO_INCREMENT = DB_CLIENT === 'postgres' ? '' : 'AUTO_INCREMENT';
// Settings ID logic: Postgres doesn't like DEFAULT 1 on INT PK without sequence easily,
// but since we insert ID 1 manually, we can just use INT PRIMARY KEY.
const TIMESTAMP_TYPE = 'TIMESTAMP'; // Both support TIMESTAMP
// 1. Settings Table
await connection.query(`
CREATE TABLE IF NOT EXISTS settings (
id INT PRIMARY KEY,
condo_name VARCHAR(255) DEFAULT 'Il Mio Condominio',
default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00,
current_year INT,
smtp_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'}
)
`);
// Migration for smtp_config if missing (Check column existence)
try {
let hasCol = false;
if (DB_CLIENT === 'postgres') {
const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings' AND column_name='smtp_config'");
hasCol = res.length > 0;
} else {
const [res] = await connection.query("SHOW COLUMNS FROM settings LIKE 'smtp_config'");
hasCol = res.length > 0;
}
if (!hasCol) {
await connection.query("ALTER TABLE settings ADD COLUMN smtp_config JSON NULL");
}
} catch (e) { console.warn("Settings migration check failed", e.message); }
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, condo_name, default_monthly_quota, current_year) VALUES (1, ?, ?, ?)',
['Condominio Demo', 100.00, currentYear]
);
}
// 2. Families Table
await connection.query(`
CREATE TABLE IF NOT EXISTS families (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
unit_number VARCHAR(50),
contact_email VARCHAR(255),
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP
)
`);
// 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
)
`);
// --- MIGRATION: CHECK FOR PHONE & ALERTS COLUMNS ---
try {
let hasPhone = false;
let hasAlerts = false;
if (DB_CLIENT === 'postgres') {
const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='users'");
hasPhone = cols.some(c => c.column_name === 'phone');
hasAlerts = cols.some(c => c.column_name === 'receive_alerts');
} else {
const [cols] = await connection.query("SHOW COLUMNS FROM users");
hasPhone = cols.some(c => c.Field === 'phone');
hasAlerts = cols.some(c => c.Field === 'receive_alerts');
}
if (!hasPhone) {
console.log('Adding missing "phone" column to users table...');
await connection.query("ALTER TABLE users ADD COLUMN phone VARCHAR(20)");
}
if (!hasAlerts) {
console.log('Adding missing "receive_alerts" column to users table...');
await connection.query("ALTER TABLE users ADD COLUMN receive_alerts BOOLEAN DEFAULT TRUE");
}
} catch (migError) {
console.warn("Migration check failed:", migError.message);
}
// 5. Alerts Table
await connection.query(`
CREATE TABLE IF NOT EXISTS alerts (
id VARCHAR(36) PRIMARY KEY,
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
)
`);
// Seed Admin User
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.');
}
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 };