- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration - Frontend: React 18 + Vite + TailwindCSS/DaisyUI - Security fixes applied (2026-07 + 2026-08): - LDAP injection prevention, CSRF protection, HttpOnly cookies - Session hashing (SHA-256), account lockout, rate limiting - Input validation (zod), file upload security, CSP/HSTS headers - V3: express-rate-limit updated (ip-address SSRF fix) - V4: postcss updated (nanoid DoS fix) - V5: Rate-limit on /health endpoint - V6: Session rotation on login (session fixation prevention) - V9: Task values array limit (DoS prevention) - V10: Frontend XSS audit completed - Docker: Multi-stage build, non-root user, PostgreSQL + backup service
83 lines
3.4 KiB
JavaScript
83 lines
3.4 KiB
JavaScript
/**
|
|
* Stats and audit-log routes module.
|
|
*
|
|
* Punkt 7: Single aggregated query for stats instead of 9 nested callbacks.
|
|
*/
|
|
const express = require('express');
|
|
const db = require('../db');
|
|
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
|
|
const { validateQuery, paginationSchema } = require('../middleware/validation');
|
|
|
|
const router = express.Router();
|
|
|
|
router.use(authMiddleware);
|
|
router.use(adminMiddleware);
|
|
|
|
// Punkt 7: Single aggregated stats query
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
const rawStats = await db.prepare(`
|
|
SELECT
|
|
(SELECT COUNT(*) FROM users WHERE status = 'aktiv') as activeUsers,
|
|
(SELECT COUNT(*) FROM users) as totalUsers,
|
|
(SELECT COUNT(*) FROM tasks WHERE status = 'offen') as openTasks,
|
|
(SELECT COUNT(*) FROM tasks WHERE status = 'erledigt') as completedTasks,
|
|
(SELECT COUNT(*) FROM tasks) as totalTasks,
|
|
(SELECT COUNT(*) FROM templates) as totalTemplates,
|
|
(SELECT COUNT(*) FROM templates WHERE is_assignable = 1) as assignableTemplates,
|
|
(SELECT COUNT(*) FROM users WHERE source = 'ad') as adUsers,
|
|
(SELECT COUNT(*) FROM users WHERE source = 'local') as localUsers
|
|
`).get();
|
|
|
|
// PostgreSQL lowercases aliases; normalize keys and coerce counts to numbers.
|
|
const normalizeKey = (key) => key.toLowerCase();
|
|
const keyMap = {
|
|
activeusers: 'activeUsers',
|
|
totalusers: 'totalUsers',
|
|
opentasks: 'openTasks',
|
|
completedtasks: 'completedTasks',
|
|
totaltasks: 'totalTasks',
|
|
totaltemplates: 'totalTemplates',
|
|
assignabletemplates: 'assignableTemplates',
|
|
adusers: 'adUsers',
|
|
localusers: 'localUsers'
|
|
};
|
|
const stats = {};
|
|
for (const [key, value] of Object.entries(rawStats)) {
|
|
const normalized = normalizeKey(key);
|
|
const newKey = keyMap[normalized] || normalized;
|
|
stats[newKey] = typeof value === 'string' ? Number(value) : value;
|
|
}
|
|
|
|
const topTemplates = await db.prepare(
|
|
'SELECT t.id, t.name, COUNT(tk.id) as task_count FROM templates t LEFT JOIN tasks tk ON t.id = tk.template_id GROUP BY t.id ORDER BY task_count DESC LIMIT 5'
|
|
).all();
|
|
|
|
const recentActivity = await db.prepare(
|
|
'SELECT al.*, u.name as user_name, u.email as user_email FROM audit_log al LEFT JOIN users u ON al.user_id = u.id ORDER BY al.created_at DESC LIMIT 10'
|
|
).all();
|
|
|
|
res.json({ ...stats, topTemplates, recentActivity });
|
|
} catch (err) {
|
|
console.error('[ERROR] GET /stats -', err.message);
|
|
res.status(500).json({ error: 'Interner Serverfehler.' });
|
|
}
|
|
});
|
|
|
|
// Audit log with pagination (Punkt 16: bounded limits)
|
|
router.get('/audit-log', validateQuery(paginationSchema), async (req, res) => {
|
|
const { page, limit } = req.validatedQuery;
|
|
const offset = (page - 1) * limit;
|
|
|
|
try {
|
|
const rows = await db.prepare('SELECT al.*, u.name as user_name, u.email as user_email FROM audit_log al LEFT JOIN users u ON al.user_id = u.id ORDER BY al.created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
|
|
const countRow = await db.prepare('SELECT COUNT(*) as total FROM audit_log').get();
|
|
const total = countRow?.total || 0;
|
|
res.json({ entries: rows, total, page, limit, totalPages: Math.ceil(total / limit) });
|
|
} catch (err) {
|
|
console.error('[ERROR] GET /audit-log -', err.message);
|
|
res.status(500).json({ error: 'Interner Serverfehler.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router; |