The changes address several issues related to data persistence and security within the Condopay application. **Settings Persistence:** - **Condo Creation:** Corrected the logic for creating new condos. The system now correctly handles passing an empty string for the `id` when creating a new condo, allowing the backend service to generate the ID, rather than attempting to create a new ID on the frontend. - **Family Quota Parsing:** Enhanced the parsing of `customMonthlyQuota` for families to safely handle empty or whitespace-only input, preventing potential errors during data submission. **Authentication and Authorization:** - **Admin Role Enforcement:** Ensured that the default admin user created during database initialization always has the 'admin' role, even if it was previously changed or created incorrectly. - **Token Verification Error Handling:** Modified the JWT token verification to return a `401 Unauthorized` status for all token-related errors (e.g., expired, invalid). This will prompt the frontend to log out the user more effectively. - **Admin Access Logging:** Added console warnings when non-admin users attempt to access admin-only routes, providing better visibility into potential access control issues. **Infrastructure:** - **Docker Cleanup:** Removed unused and outdated Dockerfiles and `.dockerignore` content, streamlining the build process and removing potential confusion. These improvements enhance the reliability of data management for condos and families, strengthen security by ensuring proper role enforcement and error handling, and clean up the development infrastructure.
379 lines
18 KiB
JavaScript
379 lines
18 KiB
JavaScript
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const bodyParser = require('body-parser');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { pool, initDb } = require('./db');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const nodemailer = require('nodemailer');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_key_123';
|
|
|
|
app.use(cors());
|
|
app.use(bodyParser.json());
|
|
|
|
// --- EMAIL & SCHEDULER (Same as before) ---
|
|
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) return;
|
|
|
|
const config = settings[0].smtp_config;
|
|
if (!config.host || !config.user || !config.pass) return;
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: config.host,
|
|
port: config.port,
|
|
secure: config.secure,
|
|
auth: { user: config.user, pass: config.pass },
|
|
});
|
|
|
|
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,
|
|
subject: subject,
|
|
text: body,
|
|
});
|
|
} catch (error) { console.error('Email error:', error.message); }
|
|
}
|
|
|
|
// --- MIDDLEWARE ---
|
|
const authenticateToken = (req, res, next) => {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (!token) return res.sendStatus(401);
|
|
|
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
|
// Return 401 for token errors (expired/invalid) to trigger frontend logout
|
|
if (err) {
|
|
console.error("Token verification failed:", err.message);
|
|
return res.sendStatus(401);
|
|
}
|
|
req.user = user;
|
|
next();
|
|
});
|
|
};
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
if (req.user && req.user.role === 'admin') {
|
|
next();
|
|
} else {
|
|
console.warn(`Access denied for user ${req.user?.email} with role ${req.user?.role}`);
|
|
res.status(403).json({ message: 'Access denied: Admins only' });
|
|
}
|
|
};
|
|
|
|
// --- 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' });
|
|
|
|
// Ensure role is captured correctly from DB
|
|
const token = jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role, familyId: user.family_id },
|
|
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 }
|
|
});
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- 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);
|
|
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: { ...updatedUser[0], receiveAlerts: !!updatedUser[0].receive_alerts } });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- 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({ 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 { currentYear, smtpConfig } = req.body;
|
|
try {
|
|
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 }); }
|
|
});
|
|
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);
|
|
res.json(Array.from(years).sort((a, b) => b - a));
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- CONDOS ---
|
|
app.get('/api/condos', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM condos');
|
|
res.json(rows.map(r => ({
|
|
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 }); }
|
|
});
|
|
app.post('/api/condos', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { name, address, defaultMonthlyQuota } = req.body;
|
|
const id = uuidv4();
|
|
try {
|
|
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/condos/:id', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { name, address, defaultMonthlyQuota } = req.body;
|
|
try {
|
|
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 }); }
|
|
});
|
|
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 ---
|
|
app.get('/api/families', authenticateToken, async (req, res) => {
|
|
try {
|
|
let query = `SELECT f.* FROM families f`;
|
|
let params = [];
|
|
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);
|
|
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, condoId, customMonthlyQuota } = req.body;
|
|
const id = uuidv4();
|
|
try {
|
|
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 { name, unitNumber, contactEmail, customMonthlyQuota } = req.body;
|
|
try {
|
|
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 }); }
|
|
});
|
|
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 }); }
|
|
});
|
|
|
|
// --- 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 {
|
|
await pool.query('INSERT IGNORE INTO notice_reads (user_id, notice_id, read_at) VALUES (?, ?, NOW())', [userId, req.params.id]);
|
|
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 {
|
|
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) {
|
|
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 }); }
|
|
});
|
|
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;
|
|
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]);
|
|
res.json({ id, familyId, amount, datePaid, forMonth, forYear, notes });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- 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 }); }
|
|
});
|
|
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]);
|
|
res.json({ success: true, id });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
app.put('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { email, role, familyId, name, phone, password, receiveAlerts } = req.body;
|
|
try {
|
|
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(req.params.id);
|
|
await pool.query(query, params);
|
|
res.json({ success: true });
|
|
} 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 }); }
|
|
});
|
|
|
|
// --- 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 }); }
|
|
});
|
|
|
|
initDb().then(() => {
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
});
|