fix: Improve settings persistence and auth handling
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.
This commit is contained in:
BIN
.dockerignore
BIN
.dockerignore
Binary file not shown.
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
|||||||
# Stage 1: Build
|
|
||||||
FROM node:20-alpine as build
|
|
||||||
WORKDIR /app
|
|
||||||
# Copia package.json
|
|
||||||
COPY package*.json ./
|
|
||||||
# Usa npm install invece di ci per evitare errori se manca package-lock.json
|
|
||||||
RUN npm install
|
|
||||||
# Copia tutto il resto
|
|
||||||
COPY . .
|
|
||||||
# Esegui la build (genera la cartella dist)
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Serve con Nginx
|
|
||||||
FROM nginx:alpine
|
|
||||||
# Copia la build di React nella cartella di Nginx
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
|
||||||
# Copia la configurazione custom di Nginx
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
|
|||||||
23
nginx.conf
23
nginx.conf
@@ -1,22 +1 @@
|
|||||||
server {
|
<EFBFBD><EFBFBD><EFBFBD>z
|
||||||
listen 80;
|
|
||||||
|
|
||||||
# Serve i file statici del frontend React
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
# Fondamentale per il routing client-side (SPA)
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy per le chiamate API verso il backend
|
|
||||||
location /api {
|
|
||||||
# 'backend' deve corrispondere al nome del servizio nel docker-compose.yml
|
|
||||||
proxy_pass http://backend:3001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -224,7 +224,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
const handleCondoSubmit = async (e: React.FormEvent) => {
|
const handleCondoSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
// FIX: Do not generate ID for new condo, let backend/service handle it (POST vs PUT check)
|
// If editingCondo exists, use its ID. If not, empty string tells service to create new.
|
||||||
const payload: Condo = {
|
const payload: Condo = {
|
||||||
id: editingCondo ? editingCondo.id : '',
|
id: editingCondo ? editingCondo.id : '',
|
||||||
name: condoForm.name,
|
name: condoForm.name,
|
||||||
@@ -247,7 +247,10 @@ export const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
setShowCondoModal(false);
|
setShowCondoModal(false);
|
||||||
window.dispatchEvent(new Event('condo-updated'));
|
window.dispatchEvent(new Event('condo-updated'));
|
||||||
} catch (e) { console.error(e); alert("Errore nel salvataggio del condominio"); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Errore nel salvataggio del condominio. Assicurati di essere amministratore.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCondo = async (id: string) => {
|
const handleDeleteCondo = async (id: string) => {
|
||||||
@@ -288,7 +291,10 @@ export const SettingsPage: React.FC = () => {
|
|||||||
const handleFamilySubmit = async (e: React.FormEvent) => {
|
const handleFamilySubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const quota = familyForm.customMonthlyQuota ? parseFloat(familyForm.customMonthlyQuota) : undefined;
|
// Handle parsing safely
|
||||||
|
const quota = familyForm.customMonthlyQuota && familyForm.customMonthlyQuota.trim() !== ''
|
||||||
|
? parseFloat(familyForm.customMonthlyQuota)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (editingFamily) {
|
if (editingFamily) {
|
||||||
const updatedFamily = {
|
const updatedFamily = {
|
||||||
@@ -312,7 +318,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
setShowFamilyModal(false);
|
setShowFamilyModal(false);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(`Errore: ${e.message || "Impossibile salvare la famiglia"}`);
|
alert(`Errore: ${e.message || "Impossibile salvare la famiglia. Controlla i permessi."}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -345,7 +351,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
setUsers(await CondoService.getUsers());
|
setUsers(await CondoService.getUsers());
|
||||||
setShowUserModal(false);
|
setShowUserModal(false);
|
||||||
} catch (e) { alert("Errore"); }
|
} catch (e) { alert("Errore nel salvataggio utente"); }
|
||||||
};
|
};
|
||||||
const handleDeleteUser = async (id: string) => {
|
const handleDeleteUser = async (id: string) => {
|
||||||
if(!window.confirm("Eliminare utente?")) return;
|
if(!window.confirm("Eliminare utente?")) return;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM node:20-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
# Copia package.json del server
|
|
||||||
COPY package*.json ./
|
|
||||||
# Installa le dipendenze
|
|
||||||
RUN npm install
|
|
||||||
# Copia il codice sorgente del server
|
|
||||||
COPY . .
|
|
||||||
EXPOSE 3001
|
|
||||||
CMD ["npm", "start"]
|
|
||||||
|
|||||||
@@ -114,9 +114,6 @@ const initDb = async () => {
|
|||||||
if (!hasCondoId) {
|
if (!hasCondoId) {
|
||||||
console.log('Migrating: Adding condo_id to families...');
|
console.log('Migrating: Adding condo_id to families...');
|
||||||
await connection.query("ALTER TABLE families ADD COLUMN condo_id VARCHAR(36)");
|
await connection.query("ALTER TABLE families ADD COLUMN condo_id VARCHAR(36)");
|
||||||
if (DB_CLIENT !== 'postgres') { // Add FK for mysql specifically if needed, simplified here
|
|
||||||
// await connection.query("ALTER TABLE families ADD CONSTRAINT fk_condo FOREIGN KEY (condo_id) REFERENCES condos(id)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!hasQuota) {
|
if (!hasQuota) {
|
||||||
console.log('Migrating: Adding custom_monthly_quota to families...');
|
console.log('Migrating: Adding custom_monthly_quota to families...');
|
||||||
@@ -207,6 +204,7 @@ const initDb = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ENSURE ADMIN EXISTS AND HAS CORRECT ROLE
|
||||||
const [admins] = await connection.query('SELECT * FROM users WHERE email = ?', ['fcarra79@gmail.com']);
|
const [admins] = await connection.query('SELECT * FROM users WHERE email = ?', ['fcarra79@gmail.com']);
|
||||||
if (admins.length === 0) {
|
if (admins.length === 0) {
|
||||||
const hashedPassword = await bcrypt.hash('Mr10921.', 10);
|
const hashedPassword = await bcrypt.hash('Mr10921.', 10);
|
||||||
@@ -216,6 +214,10 @@ const initDb = async () => {
|
|||||||
[uuidv4(), 'fcarra79@gmail.com', hashedPassword, 'Amministratore', 'admin']
|
[uuidv4(), 'fcarra79@gmail.com', hashedPassword, 'Amministratore', 'admin']
|
||||||
);
|
);
|
||||||
console.log('Default Admin user created.');
|
console.log('Default Admin user created.');
|
||||||
|
} else {
|
||||||
|
// Force update role to admin just in case it was changed or created wrongly
|
||||||
|
await connection.query('UPDATE users SET role = ? WHERE email = ?', ['admin', 'fcarra79@gmail.com']);
|
||||||
|
console.log('Ensured default user has admin role.');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Database tables initialized.');
|
console.log('Database tables initialized.');
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ app.use(cors());
|
|||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
// --- EMAIL & SCHEDULER (Same as before) ---
|
// --- EMAIL & SCHEDULER (Same as before) ---
|
||||||
// ... (Keeping simple for brevity, logic remains same but using pool)
|
|
||||||
async function sendEmailToUsers(subject, body) {
|
async function sendEmailToUsers(subject, body) {
|
||||||
try {
|
try {
|
||||||
const [settings] = await pool.query('SELECT smtp_config FROM settings WHERE id = 1');
|
const [settings] = await pool.query('SELECT smtp_config FROM settings WHERE id = 1');
|
||||||
@@ -44,22 +43,32 @@ async function sendEmailToUsers(subject, body) {
|
|||||||
});
|
});
|
||||||
} catch (error) { console.error('Email error:', error.message); }
|
} catch (error) { console.error('Email error:', error.message); }
|
||||||
}
|
}
|
||||||
// ... Scheduler logic ...
|
|
||||||
|
|
||||||
// --- MIDDLEWARE ---
|
// --- MIDDLEWARE ---
|
||||||
const authenticateToken = (req, res, next) => {
|
const authenticateToken = (req, res, next) => {
|
||||||
const authHeader = req.headers['authorization'];
|
const authHeader = req.headers['authorization'];
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
if (!token) return res.sendStatus(401);
|
if (!token) return res.sendStatus(401);
|
||||||
|
|
||||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||||
if (err) return res.sendStatus(403);
|
// 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;
|
req.user = user;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const requireAdmin = (req, res, next) => {
|
const requireAdmin = (req, res, next) => {
|
||||||
if (req.user && req.user.role === 'admin') next();
|
if (req.user && req.user.role === 'admin') {
|
||||||
else res.status(403).json({ message: 'Access denied: Admins only' });
|
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 ---
|
// --- AUTH ---
|
||||||
@@ -72,6 +81,7 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
const validPassword = await bcrypt.compare(password, user.password_hash);
|
const validPassword = await bcrypt.compare(password, user.password_hash);
|
||||||
if (!validPassword) return res.status(401).json({ message: 'Invalid credentials' });
|
if (!validPassword) return res.status(401).json({ message: 'Invalid credentials' });
|
||||||
|
|
||||||
|
// Ensure role is captured correctly from DB
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, email: user.email, role: user.role, familyId: user.family_id },
|
{ id: user.id, email: user.email, role: user.role, familyId: user.family_id },
|
||||||
JWT_SECRET, { expiresIn: '24h' }
|
JWT_SECRET, { expiresIn: '24h' }
|
||||||
@@ -237,9 +247,7 @@ app.delete('/api/notices/:id', authenticateToken, requireAdmin, async (req, res)
|
|||||||
app.post('/api/notices/:id/read', authenticateToken, async (req, res) => {
|
app.post('/api/notices/:id/read', authenticateToken, async (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
try {
|
try {
|
||||||
// Ignore duplicate reads
|
|
||||||
await pool.query('INSERT IGNORE INTO notice_reads (user_id, notice_id, read_at) VALUES (?, ?, NOW())', [userId, req.params.id]);
|
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 });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user