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.
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copia i file di dipendenze del server
|
||||
COPY package*.json ./
|
||||
|
||||
# Installa le dipendenze
|
||||
RUN npm install
|
||||
|
||||
# Copia il codice sorgente del server
|
||||
COPY . .
|
||||
|
||||
# Espone la porta definita nel server.js
|
||||
EXPOSE 3001
|
||||
|
||||
# Avvia il server
|
||||
CMD ["npm", "start"]
|
||||
|
||||
141
server/db.js
141
server/db.js
@@ -1,36 +1,106 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
require('dotenv').config();
|
||||
|
||||
// Configuration from .env or defaults
|
||||
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 || 3306,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
port: process.env.DB_PORT || (DB_CLIENT === 'postgres' ? 5432 : 3306),
|
||||
};
|
||||
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
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 pool.getConnection();
|
||||
console.log('Database connected successfully.');
|
||||
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 DEFAULT 1,
|
||||
id INT PRIMARY KEY,
|
||||
condo_name VARCHAR(255) DEFAULT 'Il Mio Condominio',
|
||||
default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00,
|
||||
current_year INT
|
||||
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();
|
||||
@@ -47,7 +117,7 @@ const initDb = async () => {
|
||||
name VARCHAR(255) NOT NULL,
|
||||
unit_number VARCHAR(50),
|
||||
contact_email VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -57,11 +127,11 @@ const initDb = async () => {
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
family_id VARCHAR(36) NOT NULL,
|
||||
amount DECIMAL(10, 2) NOT NULL,
|
||||
date_paid DATETIME NOT NULL,
|
||||
date_paid ${TIMESTAMP_TYPE} NOT NULL,
|
||||
for_month INT NOT NULL,
|
||||
for_year INT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
@@ -76,23 +146,54 @@ const initDb = async () => {
|
||||
role VARCHAR(20) DEFAULT 'user',
|
||||
phone VARCHAR(20),
|
||||
family_id VARCHAR(36) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
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 COLUMN ---
|
||||
// This ensures existing databases get the new column without dropping the table
|
||||
// --- MIGRATION: CHECK FOR PHONE & ALERTS COLUMNS ---
|
||||
try {
|
||||
const [columns] = await connection.query("SHOW COLUMNS FROM users LIKE 'phone'");
|
||||
if (columns.length === 0) {
|
||||
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) {
|
||||
@@ -106,11 +207,11 @@ const initDb = async () => {
|
||||
}
|
||||
|
||||
console.log('Database tables initialized.');
|
||||
connection.release();
|
||||
if (connection.release) connection.release();
|
||||
} catch (error) {
|
||||
console.error('Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { pool, initDb };
|
||||
module.exports = { pool: dbInterface, initDb };
|
||||
@@ -15,6 +15,9 @@
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.9.2",
|
||||
"pg": "^8.11.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.9.13",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
233
server/server.js
233
server/server.js
@@ -4,7 +4,8 @@ const bodyParser = require('body-parser');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { pool, initDb } = require('./db');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -13,6 +14,107 @@ const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_key_123';
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// --- EMAIL SERVICE & SCHEDULER ---
|
||||
|
||||
// Function to send email
|
||||
async function sendEmailToUsers(subject, body) {
|
||||
try {
|
||||
const [settings] = await pool.query('SELECT smtp_config FROM settings WHERE id = 1');
|
||||
if (!settings.length || !settings[0].smtp_config) {
|
||||
console.log('No SMTP config found, skipping email.');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = settings[0].smtp_config;
|
||||
// Basic validation
|
||||
if (!config.host || !config.user || !config.pass) return;
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: config.secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: config.user,
|
||||
pass: config.pass,
|
||||
},
|
||||
});
|
||||
|
||||
// Get users who opted in
|
||||
const [users] = await pool.query('SELECT email FROM users WHERE receive_alerts = TRUE AND email IS NOT NULL AND email != ""');
|
||||
|
||||
if (users.length === 0) return;
|
||||
|
||||
const bccList = users.map(u => u.email).join(',');
|
||||
|
||||
await transporter.sendMail({
|
||||
from: config.fromEmail || config.user,
|
||||
bcc: bccList, // Blind copy to all users
|
||||
subject: subject,
|
||||
text: body, // Plain text for now
|
||||
// html: body // Could add HTML support later
|
||||
});
|
||||
|
||||
console.log(`Alert sent to ${users.length} users.`);
|
||||
} catch (error) {
|
||||
console.error('Email sending failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Scheduler (Simulating Cron)
|
||||
// In production, use 'node-cron' or similar. Here we use setInterval for simplicity in this environment
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
|
||||
// 1. Get Active Alerts for this hour
|
||||
const [alerts] = await pool.query('SELECT * FROM alerts WHERE active = TRUE AND send_hour = ?', [currentHour]);
|
||||
|
||||
for (const alert of alerts) {
|
||||
let shouldSend = false;
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
|
||||
// Determine Target Date based on logic
|
||||
// "before_next_month": Check if today is (LastDayOfMonth - days_offset)
|
||||
// "after_current_month": Check if today is (FirstDayOfMonth + days_offset)
|
||||
|
||||
if (alert.offset_type === 'before_next_month') {
|
||||
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1);
|
||||
const targetDate = new Date(nextMonth);
|
||||
targetDate.setDate(targetDate.getDate() - alert.days_offset);
|
||||
|
||||
if (today.getTime() === targetDate.getTime()) shouldSend = true;
|
||||
|
||||
} else if (alert.offset_type === 'after_current_month') {
|
||||
const thisMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const targetDate = new Date(thisMonth);
|
||||
targetDate.setDate(targetDate.getDate() + alert.days_offset);
|
||||
|
||||
if (today.getTime() === targetDate.getTime()) shouldSend = true;
|
||||
}
|
||||
|
||||
// Check if already sent today (to prevent double send if interval restarts)
|
||||
if (shouldSend) {
|
||||
const lastSent = alert.last_sent ? new Date(alert.last_sent) : null;
|
||||
if (lastSent && lastSent.toDateString() === today.toDateString()) {
|
||||
shouldSend = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSend) {
|
||||
console.log(`Triggering alert: ${alert.subject}`);
|
||||
await sendEmailToUsers(alert.subject, alert.body);
|
||||
await pool.query('UPDATE alerts SET last_sent = NOW() WHERE id = ?', [alert.id]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Scheduler error:", e);
|
||||
}
|
||||
}, 60 * 60 * 1000); // Check every hour (approx)
|
||||
|
||||
|
||||
// --- MIDDLEWARE ---
|
||||
|
||||
const authenticateToken = (req, res, next) => {
|
||||
@@ -61,7 +163,8 @@ app.post('/api/auth/login', async (req, res) => {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
familyId: user.family_id
|
||||
familyId: user.family_id,
|
||||
receiveAlerts: !!user.receive_alerts
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -69,6 +172,48 @@ app.post('/api/auth/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- PROFILE ROUTES (Self-service) ---
|
||||
|
||||
app.put('/api/profile', authenticateToken, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const { name, phone, password, receiveAlerts } = req.body;
|
||||
|
||||
try {
|
||||
let query = 'UPDATE users SET name = ?, phone = ?, receive_alerts = ?';
|
||||
let params = [name, phone, receiveAlerts];
|
||||
|
||||
if (password && password.trim() !== '') {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
query += ', password_hash = ?';
|
||||
params.push(hashedPassword);
|
||||
}
|
||||
|
||||
query += ' WHERE id = ?';
|
||||
params.push(userId);
|
||||
|
||||
await pool.query(query, params);
|
||||
|
||||
// Return updated user info
|
||||
const [updatedUser] = await pool.query('SELECT id, email, name, role, phone, family_id, receive_alerts FROM users WHERE id = ?', [userId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: updatedUser[0].id,
|
||||
email: updatedUser[0].email,
|
||||
name: updatedUser[0].name,
|
||||
role: updatedUser[0].role,
|
||||
phone: updatedUser[0].phone,
|
||||
familyId: updatedUser[0].family_id,
|
||||
receiveAlerts: !!updatedUser[0].receive_alerts
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- SETTINGS ROUTES ---
|
||||
|
||||
app.get('/api/settings', authenticateToken, async (req, res) => {
|
||||
@@ -78,7 +223,8 @@ app.get('/api/settings', authenticateToken, async (req, res) => {
|
||||
res.json({
|
||||
condoName: rows[0].condo_name,
|
||||
defaultMonthlyQuota: parseFloat(rows[0].default_monthly_quota),
|
||||
currentYear: rows[0].current_year
|
||||
currentYear: rows[0].current_year,
|
||||
smtpConfig: rows[0].smtp_config || {}
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({ message: 'Settings not found' });
|
||||
@@ -89,11 +235,11 @@ app.get('/api/settings', authenticateToken, async (req, res) => {
|
||||
});
|
||||
|
||||
app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => {
|
||||
const { condoName, defaultMonthlyQuota, currentYear } = req.body;
|
||||
const { condoName, defaultMonthlyQuota, currentYear, smtpConfig } = req.body;
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE settings SET condo_name = ?, default_monthly_quota = ?, current_year = ? WHERE id = 1',
|
||||
[condoName, defaultMonthlyQuota, currentYear]
|
||||
'UPDATE settings SET condo_name = ?, default_monthly_quota = ?, current_year = ?, smtp_config = ? WHERE id = 1',
|
||||
[condoName, defaultMonthlyQuota, currentYear, JSON.stringify(smtpConfig)]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
@@ -117,6 +263,64 @@ app.get('/api/years', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- ALERTS ROUTES ---
|
||||
|
||||
app.get('/api/alerts', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM alerts');
|
||||
res.json(rows.map(r => ({
|
||||
id: r.id,
|
||||
subject: r.subject,
|
||||
body: r.body,
|
||||
daysOffset: r.days_offset,
|
||||
offsetType: r.offset_type,
|
||||
sendHour: r.send_hour,
|
||||
active: !!r.active,
|
||||
lastSent: r.last_sent
|
||||
})));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/alerts', authenticateToken, requireAdmin, async (req, res) => {
|
||||
const { subject, body, daysOffset, offsetType, sendHour, active } = req.body;
|
||||
const id = uuidv4();
|
||||
try {
|
||||
await pool.query(
|
||||
'INSERT INTO alerts (id, subject, body, days_offset, offset_type, send_hour, active) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, subject, body, daysOffset, offsetType, sendHour, active]
|
||||
);
|
||||
res.json({ id, subject, body, daysOffset, offsetType, sendHour, active });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { subject, body, daysOffset, offsetType, sendHour, active } = req.body;
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE alerts SET subject = ?, body = ?, days_offset = ?, offset_type = ?, send_hour = ?, active = ? WHERE id = ?',
|
||||
[subject, body, daysOffset, offsetType, sendHour, active, id]
|
||||
);
|
||||
res.json({ id, subject, body, daysOffset, offsetType, sendHour, active });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM alerts WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- FAMILIES ROUTES ---
|
||||
|
||||
app.get('/api/families', authenticateToken, async (req, res) => {
|
||||
@@ -264,14 +468,15 @@ app.post('/api/payments', authenticateToken, async (req, res) => {
|
||||
|
||||
app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT id, email, name, role, phone, family_id FROM users');
|
||||
const [rows] = await pool.query('SELECT id, email, name, role, phone, family_id, receive_alerts FROM users');
|
||||
res.json(rows.map(r => ({
|
||||
id: r.id,
|
||||
email: r.email,
|
||||
name: r.name,
|
||||
role: r.role,
|
||||
phone: r.phone,
|
||||
familyId: r.family_id
|
||||
familyId: r.family_id,
|
||||
receiveAlerts: !!r.receive_alerts
|
||||
})));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
@@ -279,13 +484,13 @@ app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
const { email, password, name, role, familyId, phone } = req.body;
|
||||
const { email, password, name, role, familyId, phone, receiveAlerts } = req.body;
|
||||
try {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const id = uuidv4();
|
||||
await pool.query(
|
||||
'INSERT INTO users (id, email, password_hash, name, role, family_id, phone) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, email, hashedPassword, name, role || 'user', familyId || null, phone]
|
||||
'INSERT INTO users (id, email, password_hash, name, role, family_id, phone, receive_alerts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, email, hashedPassword, name, role || 'user', familyId || null, phone, receiveAlerts]
|
||||
);
|
||||
res.json({ success: true, id });
|
||||
} catch (e) {
|
||||
@@ -295,12 +500,12 @@ app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||
|
||||
app.put('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { email, role, familyId, name, phone, password } = req.body;
|
||||
const { email, role, familyId, name, phone, password, receiveAlerts } = req.body;
|
||||
|
||||
try {
|
||||
// Prepare update query dynamically based on whether password is being changed
|
||||
let query = 'UPDATE users SET email = ?, role = ?, family_id = ?, name = ?, phone = ?';
|
||||
let params = [email, role, familyId || null, name, phone];
|
||||
let query = 'UPDATE users SET email = ?, role = ?, family_id = ?, name = ?, phone = ?, receive_alerts = ?';
|
||||
let params = [email, role, familyId || null, name, phone, receiveAlerts];
|
||||
|
||||
if (password && password.trim() !== '') {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
Reference in New Issue
Block a user