Files
Condopay/server/server.js
frakarr 79e249b638 feat: Setup project with Vite and React
Initializes the Condopay frontend project using Vite, React, and TypeScript. Includes basic project structure, dependencies, and configuration for Tailwind CSS and React Router.
2025-12-06 18:55:48 +01:00

335 lines
9.8 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 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());
// --- MIDDLEWARE ---
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
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' });
}
};
// --- AUTH ROUTES ---
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' }
);
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
familyId: user.family_id
}
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// --- SETTINGS ROUTES ---
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
});
} 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 } = req.body;
try {
await pool.query(
'UPDATE settings SET condo_name = ?, default_monthly_quota = ?, current_year = ? WHERE id = 1',
[condoName, defaultMonthlyQuota, currentYear]
);
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 });
}
});
// --- FAMILIES ROUTES ---
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 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
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 });
}
});
app.post('/api/families', authenticateToken, requireAdmin, async (req, res) => {
const { name, unitNumber, contactEmail } = 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 });
}
});
app.put('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => {
const { id } = req.params;
const { name, unitNumber, contactEmail } = 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]);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// --- PAYMENTS ROUTES ---
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) {
// 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 });
}
});
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"});
}
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 ROUTES ---
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');
res.json(rows.map(r => ({
id: r.id,
email: r.email,
name: r.name,
role: r.role,
phone: r.phone,
familyId: r.family_id
})));
} 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 } = 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]
);
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 { id } = req.params;
const { email, role, familyId, name, phone, password } = 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];
if (password && password.trim() !== '') {
const hashedPassword = await bcrypt.hash(password, 10);
query += ', password_hash = ?';
params.push(hashedPassword);
}
query += ' WHERE id = ?';
params.push(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 });
}
});
// Start Server
initDb().then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});