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.
This commit is contained in:
0
server/Dockerfile
Normal file
0
server/Dockerfile
Normal file
116
server/db.js
Normal file
116
server/db.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const bcrypt = require('bcryptjs');
|
||||
require('dotenv').config();
|
||||
|
||||
// Configuration from .env or defaults
|
||||
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
|
||||
};
|
||||
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
|
||||
const initDb = async () => {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
console.log('Database connected successfully.');
|
||||
|
||||
// 1. Settings Table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
condo_name VARCHAR(255) DEFAULT 'Il Mio Condominio',
|
||||
default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00,
|
||||
current_year INT
|
||||
)
|
||||
`);
|
||||
|
||||
const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1');
|
||||
if (rows.length === 0) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
await connection.query(
|
||||
'INSERT INTO settings (id, condo_name, default_monthly_quota, current_year) VALUES (1, ?, ?, ?)',
|
||||
['Condominio Demo', 100.00, currentYear]
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Families Table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS families (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
unit_number VARCHAR(50),
|
||||
contact_email VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// 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 DATETIME NOT NULL,
|
||||
for_month INT NOT NULL,
|
||||
for_year INT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP 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,
|
||||
created_at TIMESTAMP 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
|
||||
try {
|
||||
const [columns] = await connection.query("SHOW COLUMNS FROM users LIKE 'phone'");
|
||||
if (columns.length === 0) {
|
||||
console.log('Adding missing "phone" column to users table...');
|
||||
await connection.query("ALTER TABLE users ADD COLUMN phone VARCHAR(20)");
|
||||
}
|
||||
} catch (migError) {
|
||||
console.warn("Migration check failed:", migError.message);
|
||||
}
|
||||
|
||||
// Seed Admin User
|
||||
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.');
|
||||
}
|
||||
|
||||
console.log('Database tables initialized.');
|
||||
connection.release();
|
||||
} catch (error) {
|
||||
console.error('Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { pool, initDb };
|
||||
20
server/package.json
Normal file
20
server/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "condo-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API for CondoPay",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.9.2",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
335
server/server.js
Normal file
335
server/server.js
Normal file
@@ -0,0 +1,335 @@
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user