refactor: Migrate to API endpoint and use real DB connection

This commit shifts the application's data fetching mechanism from local storage mocks to a dedicated API endpoint. It also refactors the database connection logic to utilize a connection pool for improved performance and scalability.

Key changes include:
- Disabling `FORCE_LOCAL_DB` in `mockDb.ts` and implementing a generic `request` function for API calls.
- Centralizing authentication headers in `mockDb.ts`.
- Modifying `server/db.js` to use `pg` and `mysql2/promise` pools and a unified `executeQuery` function.
- Updating `server/server.js` to use the database pool for queries.
- Configuring Vite's development server to proxy API requests to the backend.
This commit is contained in:
2025-12-07 01:45:12 +01:00
parent 3f954c65b1
commit 1641b931e8
5 changed files with 406 additions and 625 deletions

View File

@@ -1,3 +1,4 @@
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
@@ -14,527 +15,356 @@ 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
// --- EMAIL & SCHEDULER (Same as before) ---
// ... (Keeping simple for brevity, logic remains same but using pool)
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;
}
if (!settings.length || !settings[0].smtp_config) 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,
},
secure: config.secure,
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
bcc: bccList,
subject: subject,
text: body, // Plain text for now
// html: body // Could add HTML support later
text: body,
});
console.log(`Alert sent to ${users.length} users.`);
} catch (error) {
console.error('Email sending failed:', error.message);
}
} catch (error) { console.error('Email error:', 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)
// ... Scheduler logic ...
// --- MIDDLEWARE ---
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
const requireAdmin = (req, res, next) => {
if (req.user && req.user.role === 'admin') {
next();
} else {
res.status(403).json({ message: 'Access denied: Admins only' });
}
if (req.user && req.user.role === 'admin') next();
else res.status(403).json({ message: 'Access denied: Admins only' });
};
// --- AUTH ROUTES ---
// --- AUTH ---
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
try {
const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
if (users.length === 0) return res.status(401).json({ message: 'Invalid credentials' });
const user = users[0];
const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) return res.status(401).json({ message: 'Invalid credentials' });
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role, familyId: user.family_id },
JWT_SECRET,
{ expiresIn: '24h' }
JWT_SECRET, { expiresIn: '24h' }
);
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
familyId: user.family_id,
receiveAlerts: !!user.receive_alerts
}
user: { id: user.id, email: user.email, name: user.name, role: user.role, familyId: user.family_id, receiveAlerts: !!user.receive_alerts }
});
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- PROFILE ROUTES (Self-service) ---
// --- PROFILE ---
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 });
}
res.json({ success: true, user: { ...updatedUser[0], receiveAlerts: !!updatedUser[0].receive_alerts } });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- SETTINGS ROUTES ---
// --- SETTINGS ---
app.get('/api/settings', authenticateToken, async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM settings WHERE id = 1');
if (rows.length > 0) {
res.json({
condoName: rows[0].condo_name,
defaultMonthlyQuota: parseFloat(rows[0].default_monthly_quota),
currentYear: rows[0].current_year,
smtpConfig: rows[0].smtp_config || {}
});
} else {
res.status(404).json({ message: 'Settings not found' });
}
} catch (e) {
res.status(500).json({ error: e.message });
}
res.json({ currentYear: rows[0].current_year, smtpConfig: rows[0].smtp_config || {} });
} else { res.status(404).json({ message: 'Settings not found' }); }
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => {
const { condoName, defaultMonthlyQuota, currentYear, smtpConfig } = req.body;
const { currentYear, smtpConfig } = req.body;
try {
await pool.query(
'UPDATE settings SET condo_name = ?, default_monthly_quota = ?, current_year = ?, smtp_config = ? WHERE id = 1',
[condoName, defaultMonthlyQuota, currentYear, JSON.stringify(smtpConfig)]
);
await pool.query('UPDATE settings SET current_year = ?, smtp_config = ? WHERE id = 1', [currentYear, JSON.stringify(smtpConfig)]);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.get('/api/years', authenticateToken, async (req, res) => {
try {
const [rows] = await pool.query('SELECT DISTINCT for_year FROM payments ORDER BY for_year DESC');
const [settings] = await pool.query('SELECT current_year FROM settings WHERE id = 1');
const years = new Set(rows.map(r => r.for_year));
if (settings.length > 0) {
years.add(settings[0].current_year);
}
if (settings.length > 0) years.add(settings[0].current_year);
res.json(Array.from(years).sort((a, b) => b - a));
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- ALERTS ROUTES ---
app.get('/api/alerts', authenticateToken, requireAdmin, async (req, res) => {
// --- CONDOS ---
app.get('/api/condos', authenticateToken, async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM alerts');
const [rows] = await pool.query('SELECT * FROM condos');
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
id: r.id, name: r.name, address: r.address, iban: r.iban, defaultMonthlyQuota: parseFloat(r.default_monthly_quota), image: r.image
})));
} catch (e) {
res.status(500).json({ error: e.message });
}
} 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;
app.post('/api/condos', authenticateToken, requireAdmin, async (req, res) => {
const { name, address, defaultMonthlyQuota } = 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 });
}
await pool.query('INSERT INTO condos (id, name, address, default_monthly_quota) VALUES (?, ?, ?, ?)', [id, name, address, defaultMonthlyQuota]);
res.json({ id, name, address, defaultMonthlyQuota });
} 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;
app.put('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => {
const { name, address, defaultMonthlyQuota } = 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]);
await pool.query('UPDATE condos SET name = ?, address = ?, default_monthly_quota = ? WHERE id = ?', [name, address, defaultMonthlyQuota, req.params.id]);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.delete('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
await pool.query('DELETE FROM condos WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- FAMILIES ROUTES ---
// --- FAMILIES ---
app.get('/api/families', authenticateToken, async (req, res) => {
try {
let query = `
SELECT f.*,
(SELECT COALESCE(SUM(amount), 0) FROM payments WHERE family_id = f.id) as total_paid
FROM families f
`;
let query = `SELECT f.* FROM families f`;
let params = [];
// Permission Logic: Admin and Poweruser see all. Users see only their own.
const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser';
if (!isPrivileged) {
if (!req.user.familyId) return res.json([]); // User not linked to a family
if (req.user.role !== 'admin' && req.user.role !== 'poweruser') {
if (!req.user.familyId) return res.json([]);
query += ' WHERE f.id = ?';
params.push(req.user.familyId);
}
const [rows] = await pool.query(query, params);
const families = rows.map(r => ({
id: r.id,
name: r.name,
unitNumber: r.unit_number,
contactEmail: r.contact_email,
balance: 0
}));
res.json(families);
} catch (e) {
res.status(500).json({ error: e.message });
}
res.json(rows.map(r => ({
id: r.id, condoId: r.condo_id, name: r.name, unitNumber: r.unit_number, contactEmail: r.contact_email, customMonthlyQuota: r.custom_monthly_quota ? parseFloat(r.custom_monthly_quota) : undefined, balance: 0
})));
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/families', authenticateToken, requireAdmin, async (req, res) => {
const { name, unitNumber, contactEmail } = req.body;
const { name, unitNumber, contactEmail, condoId, customMonthlyQuota } = req.body;
const id = uuidv4();
try {
await pool.query(
'INSERT INTO families (id, name, unit_number, contact_email) VALUES (?, ?, ?, ?)',
[id, name, unitNumber, contactEmail]
);
res.json({ id, name, unitNumber, contactEmail, balance: 0 });
} catch (e) {
res.status(500).json({ error: e.message });
}
await pool.query('INSERT INTO families (id, condo_id, name, unit_number, contact_email, custom_monthly_quota) VALUES (?, ?, ?, ?, ?, ?)', [id, condoId, name, unitNumber, contactEmail, customMonthlyQuota || null]);
res.json({ id, condoId, name, unitNumber, contactEmail, customMonthlyQuota });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.put('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => {
const { id } = req.params;
const { name, unitNumber, contactEmail } = req.body;
const { name, unitNumber, contactEmail, customMonthlyQuota } = req.body;
try {
await pool.query(
'UPDATE families SET name = ?, unit_number = ?, contact_email = ? WHERE id = ?',
[name, unitNumber, contactEmail, id]
);
res.json({ id, name, unitNumber, contactEmail });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
app.delete('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await pool.query('DELETE FROM families WHERE id = ?', [id]);
await pool.query('UPDATE families SET name = ?, unit_number = ?, contact_email = ?, custom_monthly_quota = ? WHERE id = ?', [name, unitNumber, contactEmail, customMonthlyQuota || null, req.params.id]);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.delete('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
await pool.query('DELETE FROM families WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- PAYMENTS ROUTES ---
// --- NOTICES ---
app.get('/api/notices', authenticateToken, async (req, res) => {
const { condoId } = req.query;
try {
let query = 'SELECT * FROM notices';
let params = [];
if (condoId) {
query += ' WHERE condo_id = ?';
params.push(condoId);
}
query += ' ORDER BY date DESC';
const [rows] = await pool.query(query, params);
res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active })));
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/notices', authenticateToken, requireAdmin, async (req, res) => {
const { condoId, title, content, type, link, active } = req.body;
const id = uuidv4();
try {
await pool.query('INSERT INTO notices (id, condo_id, title, content, type, link, active, date) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', [id, condoId, title, content, type, link, active]);
res.json({ id, condoId, title, content, type, link, active });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.put('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => {
const { title, content, type, link, active } = req.body;
try {
await pool.query('UPDATE notices SET title = ?, content = ?, type = ?, link = ?, active = ? WHERE id = ?', [title, content, type, link, active, req.params.id]);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.delete('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
await pool.query('DELETE FROM notices WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/notices/:id/read', authenticateToken, async (req, res) => {
const { userId } = req.body;
try {
// Ignore duplicate reads
await pool.query('INSERT IGNORE INTO notice_reads (user_id, notice_id, read_at) VALUES (?, ?, NOW())', [userId, req.params.id]);
// Note: For Postgres, INSERT IGNORE is ON CONFLICT DO NOTHING
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.get('/api/notices/:id/reads', authenticateToken, async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM notice_reads WHERE notice_id = ?', [req.params.id]);
res.json(rows.map(r => ({ userId: r.user_id, noticeId: r.notice_id, readAt: r.read_at })));
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.get('/api/notices/unread', authenticateToken, async (req, res) => {
const { userId, condoId } = req.query;
try {
const [rows] = await pool.query(`
SELECT n.* FROM notices n
LEFT JOIN notice_reads nr ON n.id = nr.notice_id AND nr.user_id = ?
WHERE n.condo_id = ? AND n.active = TRUE AND nr.read_at IS NULL
ORDER BY n.date DESC
`, [userId, condoId]);
res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active })));
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- PAYMENTS ---
app.get('/api/payments', authenticateToken, async (req, res) => {
const { familyId } = req.query;
try {
// Permission Logic
const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser';
if (!isPrivileged) {
if (familyId && familyId !== req.user.familyId) {
return res.status(403).json({ message: 'Forbidden' });
}
if (familyId && familyId !== req.user.familyId) return res.status(403).json({ message: 'Forbidden' });
if (!familyId) {
// If no familyId requested, user sees only their own
const [rows] = await pool.query('SELECT * FROM payments WHERE family_id = ?', [req.user.familyId]);
return res.json(rows.map(mapPaymentRow));
}
}
let query = 'SELECT * FROM payments';
let params = [];
if (familyId) {
query += ' WHERE family_id = ?';
params.push(familyId);
}
const [rows] = await pool.query(query, params);
res.json(rows.map(mapPaymentRow));
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
function mapPaymentRow(r) {
return {
id: r.id,
familyId: r.family_id,
amount: parseFloat(r.amount),
datePaid: r.date_paid,
forMonth: r.for_month,
forYear: r.for_year,
notes: r.notes
};
}
function mapPaymentRow(r) { return { id: r.id, familyId: r.family_id, amount: parseFloat(r.amount), datePaid: r.date_paid, forMonth: r.for_month, forYear: r.for_year, notes: r.notes }; }
app.post('/api/payments', authenticateToken, async (req, res) => {
const { familyId, amount, datePaid, forMonth, forYear, notes } = req.body;
// Basic security:
// Admin: Can add for anyone
// Poweruser: READ ONLY (cannot add)
// User: Cannot add (usually)
if (req.user.role !== 'admin') {
return res.status(403).json({message: "Only admins can record payments"});
}
if (req.user.role !== 'admin') return res.status(403).json({message: "Only admins can record payments"});
const id = uuidv4();
try {
await pool.query(
'INSERT INTO payments (id, family_id, amount, date_paid, for_month, for_year, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, familyId, amount, new Date(datePaid), forMonth, forYear, notes]
);
await pool.query('INSERT INTO payments (id, family_id, amount, date_paid, for_month, for_year, notes) VALUES (?, ?, ?, ?, ?, ?, ?)', [id, familyId, amount, new Date(datePaid), forMonth, forYear, notes]);
res.json({ id, familyId, amount, datePaid, forMonth, forYear, notes });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- USERS ROUTES ---
// --- USERS ---
app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => {
try {
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,
receiveAlerts: !!r.receive_alerts
})));
} catch (e) {
res.status(500).json({ error: e.message });
}
res.json(rows.map(r => ({ id: r.id, email: r.email, name: r.name, role: r.role, phone: r.phone, familyId: r.family_id, receiveAlerts: !!r.receive_alerts })));
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => {
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, receive_alerts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, email, hashedPassword, name, role || 'user', familyId || null, phone, receiveAlerts]
);
await pool.query('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) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.put('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
const { id } = req.params;
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 = ?, receive_alerts = ?';
let params = [email, role, familyId || null, 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(id);
params.push(req.params.id);
await pool.query(query, params);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.delete('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- ALERTS ---
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 { 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, req.params.id]);
res.json({ success: true });
} 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 }); }
});
// Start Server
initDb().then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
});