DEV1.0: Initial commit - Workflow Portal with security fixes

- 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
This commit is contained in:
Kühn
2026-08-24 09:45:28 +02:00
commit 6be1791c62
103 changed files with 12253 additions and 0 deletions

83
backend/routes/stats.js Normal file
View File

@@ -0,0 +1,83 @@
/**
* 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;