Files
workflow-app/backend/routes/stats.js
Kühn 1aec65dc95 Security & UX Release v7
Security:
- H1: Stored-XSS-Fix — Upload-Pfad-Whitelist (Server + Frontend-Guard safeFileUrl)
- H2: Transaktionen repariert — txDb-Contract in db.js (PG + SQLite), Rollback funktioniert
- H3-Vorbereitung: SESSION_SECRET wird in Compose durchgereicht (Fix M3)
- registerLimiter exportiert (Crash-Bug: Route.post ohne Callback)
- LDAP-Sync: PG-Transaktionsabbruch bei UNIQUE-Verstoß behoben (Precheck-Selects)
- LDAP-Filter: nur echte Benutzerkonten (keine Computer/Service-Accounts, Bit 512)
- Rollen app-seitig: Sync ändert nie role/status, neue User immer user+inaktiv
- DB-Cleanup: 82 Computer-/Service-Accounts aus lokaler User-Tabelle entfernt

UX:
- Dashboard: Vorlagen als Table-Liste + Column-Chart (Top 5 in %), 2 gleich große Spalten
- Table-Listen (Dashboard/Vorlageneditor/Aufgaben) scrollbar bis Seitenende
- Pagination 10/Seite im Dashboard, Sidebar-Label Dashboard
2026-09-10 16:57:20 +02:00

86 lines
3.6 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;
}
// Top 5 Vorlagen nach Task-Anzahl (Graph zeigt max. 5 Säulen).
// HAVING filtert Vorlagen ohne Tasks heraus, damit der Graph nur
// tatsächlich genutzte Vorlagen zeigt.
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 HAVING COUNT(tk.id) > 0 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;