Files
Condopay/server/db.js
frakarr 919be985c9 feat: Introduce app feature flags
This commit refactors the application settings to include a new `AppFeatures` interface. This allows for granular control over which features are enabled for the application.

The `AppFeatures` object includes boolean flags for:
- `multiCondo`: Enables or disables the multi-condominium management feature.
- `tickets`: Placeholder for future ticket system integration.
- `payPal`: Enables or disables PayPal payment gateway integration.
- `notices`: Enables or disables the display and management of notices.

These flags are now fetched and stored in the application state, influencing UI elements and logic across various pages to conditionally render features based on their enabled status. For example, the multi-condo selection in `Layout.tsx` and the notice display in `FamilyList.tsx` are now gated by these feature flags. The `FamilyDetail.tsx` page also uses the `payPal` flag to conditionally enable the PayPal payment option.

The `SettingsPage.tsx` has been updated to include a new 'features' tab for managing these flags.
2025-12-07 20:21:01 +01:00

367 lines
14 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: () => {}
};
} 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'}
)
`);
// Migration: Add features column if not exists
try {
let hasFeatures = 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');
} else {
const [cols] = await connection.query("SHOW COLUMNS FROM settings");
hasFeatures = cols.some(c => c.Field === 'features');
}
if (!hasFeatures) {
console.log('Migrating: Adding features to settings...');
await connection.query("ALTER TABLE settings ADD COLUMN features 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,
image VARCHAR(255),
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP
)
`);
// Migration for condos: Add new address fields and paypal_client_id
try {
let hasCity = false;
let hasPayPal = 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');
hasPayPal = cols.some(c => c.column_name === 'paypal_client_id');
} else {
const [cols] = await connection.query("SHOW COLUMNS FROM condos");
hasCity = cols.some(c => c.Field === 'city');
hasPayPal = cols.some(c => c.Field === 'paypal_client_id');
}
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");
}
if (!hasPayPal) {
console.log('Migrating: Adding PayPal fields to condos...');
await connection.query("ALTER TABLE condos ADD COLUMN paypal_client_id VARCHAR(255)");
}
} 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
)
`);
// 8. Tickets Table (Segnalazioni)
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 Table
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}, -- Base64 encoded file
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets(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
};
if (rows.length === 0) {
const currentYear = new Date().getFullYear();
await connection.query(
'INSERT INTO settings (id, current_year, features) VALUES (1, ?, ?)',
[currentYear, JSON.stringify(defaultFeatures)]
);
} 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)]);
console.log("Seeded default features settings.");
}
}
// 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 };