Files
Condopay/server/db.js
2025-12-11 23:35:36 +01:00

436 lines
15 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
});
}
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: () => {},
// Mock transaction methods for Postgres simple wrapper
// In a real prod app, you would get a specific client from the pool here
beginTransaction: async () => {},
commit: async () => {},
rollback: async () => {}
};
} 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';
const LONG_TEXT_TYPE = DB_CLIENT === 'postgres' ? 'TEXT' : 'LONGTEXT'; // For base64 files
// 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'},
features JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'},
storage_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'}
)
`);
// Migration: Add features column if not exists
try {
let hasFeatures = false;
let hasStorage = false;
if (DB_CLIENT === 'postgres') {
const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings'");
hasFeatures = cols.some(c => c.column_name === 'features');
hasStorage = cols.some(c => c.column_name === 'storage_config');
} else {
const [cols] = await connection.query("SHOW COLUMNS FROM settings");
hasFeatures = cols.some(c => c.Field === 'features');
hasStorage = cols.some(c => c.Field === 'storage_config');
}
if (!hasFeatures) {
console.log('Migrating: Adding features to settings...');
await connection.query("ALTER TABLE settings ADD COLUMN features JSON");
}
if (!hasStorage) {
console.log('Migrating: Adding storage_config to settings...');
await connection.query("ALTER TABLE settings ADD COLUMN storage_config JSON");
}
} catch(e) { console.warn("Settings migration warning:", e.message); }
// 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),
paypal_client_id VARCHAR(255),
default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00,
due_day INT DEFAULT 10,
image VARCHAR(255),
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP
)
`);
// Migration for condos due_day
try {
let hasDueDay = false;
if (DB_CLIENT === 'postgres') {
const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'");
hasDueDay = cols.some(c => c.column_name === 'due_day');
} else {
const [cols] = await connection.query("SHOW COLUMNS FROM condos");
hasDueDay = cols.some(c => c.Field === 'due_day');
}
if (!hasDueDay) {
console.log('Migrating: Adding due_day to condos...');
await connection.query("ALTER TABLE condos ADD COLUMN due_day INT DEFAULT 10");
}
} catch(e) { console.warn("Condo 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
)
`);
// 3. Payments Table
await connection.query(`
CREATE TABLE IF NOT EXISTS payments (
id VARCHAR(36) PRIMARY KEY,
family_id VARCHAR(36) NOT NULL,
extraordinary_expense_id VARCHAR(36) 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
)
`);
// 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,
target_families JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'},
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
)
`);
// 8. Tickets Table
await connection.query(`
CREATE TABLE IF NOT EXISTS tickets (
id VARCHAR(36) PRIMARY KEY,
condo_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'OPEN',
priority VARCHAR(20) DEFAULT 'MEDIUM',
category VARCHAR(20) DEFAULT 'OTHER',
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
updated_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// 9. Ticket Attachments
await connection.query(`
CREATE TABLE IF NOT EXISTS ticket_attachments (
id VARCHAR(36) PRIMARY KEY,
ticket_id VARCHAR(36) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
data ${LONG_TEXT_TYPE},
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
)
`);
// 10. Ticket Comments
await connection.query(`
CREATE TABLE IF NOT EXISTS ticket_comments (
id VARCHAR(36) PRIMARY KEY,
ticket_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
text TEXT NOT NULL,
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// 11. Extraordinary Expenses
await connection.query(`
CREATE TABLE IF NOT EXISTS extraordinary_expenses (
id VARCHAR(36) PRIMARY KEY,
condo_id VARCHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
start_date ${TIMESTAMP_TYPE},
end_date ${TIMESTAMP_TYPE},
contractor_name VARCHAR(255),
total_amount DECIMAL(15, 2) DEFAULT 0,
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
)
`);
await connection.query(`
CREATE TABLE IF NOT EXISTS expense_items (
id VARCHAR(36) PRIMARY KEY,
expense_id VARCHAR(36) NOT NULL,
description VARCHAR(255),
amount DECIMAL(15, 2) NOT NULL,
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE
)
`);
await connection.query(`
CREATE TABLE IF NOT EXISTS expense_shares (
id VARCHAR(36) PRIMARY KEY,
expense_id VARCHAR(36) NOT NULL,
family_id VARCHAR(36) NOT NULL,
percentage DECIMAL(5, 2) NOT NULL,
amount_due DECIMAL(15, 2) NOT NULL,
amount_paid DECIMAL(15, 2) DEFAULT 0,
status VARCHAR(20) DEFAULT 'UNPAID',
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE,
FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE
)
`);
await connection.query(`
CREATE TABLE IF NOT EXISTS expense_attachments (
id VARCHAR(36) PRIMARY KEY,
expense_id VARCHAR(36) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
data ${LONG_TEXT_TYPE},
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE
)
`);
// 12. CONDO ORDINARY EXPENSES (USCITE)
await connection.query(`
CREATE TABLE IF NOT EXISTS condo_expenses (
id VARCHAR(36) PRIMARY KEY,
condo_id VARCHAR(36) NOT NULL,
description VARCHAR(255) NOT NULL,
supplier_name VARCHAR(255),
amount DECIMAL(10, 2) NOT NULL,
payment_date ${TIMESTAMP_TYPE} NULL,
status VARCHAR(20) DEFAULT 'UNPAID',
payment_method VARCHAR(50),
invoice_number VARCHAR(100),
notes TEXT,
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
)
`);
await connection.query(`
CREATE TABLE IF NOT EXISTS condo_expense_attachments (
id VARCHAR(36) PRIMARY KEY,
condo_expense_id VARCHAR(36) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
data ${LONG_TEXT_TYPE},
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (condo_expense_id) REFERENCES condo_expenses(id) ON DELETE CASCADE
)
`);
// 13. DOCUMENTS (Cloud/Local)
await connection.query(`
CREATE TABLE IF NOT EXISTS documents (
id VARCHAR(36) PRIMARY KEY,
condo_id VARCHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
file_size INT,
tags JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'},
storage_provider VARCHAR(50) DEFAULT 'local_db',
file_data ${LONG_TEXT_TYPE},
upload_date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
)
`);
// --- SEEDING ---
const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1');
const defaultFeatures = {
multiCondo: true,
tickets: true,
payPal: true,
notices: true,
reports: true,
extraordinaryExpenses: true,
condoFinancialsView: false,
documents: true // Enabled by default for demo
};
const defaultStorage = {
provider: 'local_db'
};
if (rows.length === 0) {
const currentYear = new Date().getFullYear();
await connection.query(
'INSERT INTO settings (id, current_year, features, storage_config) VALUES (1, ?, ?, ?)',
[currentYear, JSON.stringify(defaultFeatures), JSON.stringify(defaultStorage)]
);
} else {
// Ensure features column has defaults if null
if (!rows[0].features) {
await connection.query('UPDATE settings SET features = ? WHERE id = 1', [JSON.stringify(defaultFeatures)]);
}
if (!rows[0].storage_config) {
await connection.query('UPDATE settings SET storage_config = ? WHERE id = 1', [JSON.stringify(defaultStorage)]);
}
}
// ENSURE ADMIN EXISTS
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']
);
} else {
await connection.query('UPDATE users SET role = ? WHERE email = ?', ['admin', 'fcarra79@gmail.com']);
}
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 };