Files
workflow-app/backend/routes/templates.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

122 lines
5.6 KiB
JavaScript

/**
* Templates routes module.
*
* Punkt 8: Uses transactions for template updates (delete+insert steps).
* Punkt 6: Uses better-sqlite3 synchronous API.
*/
const express = require('express');
const db = require('../db');
const { auditLog } = require('../auditLog');
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
const { validate, createTemplateSchema, updateTemplateSchema } = require('../middleware/validation');
const router = express.Router();
router.use(authMiddleware);
// List templates
router.get('/', async (req, res) => {
const templates = await db.prepare('SELECT * FROM templates ORDER BY id DESC').all();
if (templates.length === 0) return res.json([]);
const templateIds = templates.map(t => t.id).filter(id => Number.isInteger(id));
if (templateIds.length === 0) return res.json(templates.map(t => ({ ...t, steps: [] })));
const placeholders = templateIds.map(() => '?').join(',');
const steps = await db.prepare(`SELECT * FROM template_steps WHERE template_id IN (${placeholders}) ORDER BY step_order ASC`).all(...templateIds);
const result = templates.map(t => ({
...t,
steps: steps.filter(s => s.template_id === t.id)
}));
res.json(result);
});
// Create template (admin only) - Punkt 8: Transaction
router.post('/', adminMiddleware, validate(createTemplateSchema), async (req, res) => {
const { name, description, is_assignable, allows_file_upload, ad_create, steps } = req.validatedBody;
const assignable = is_assignable ? 1 : 0;
const fileUpload = allows_file_upload ? 1 : 0;
const adCreate = ad_create ? 1 : 0;
// H2: Alle Statements innerhalb der Transaktion über txDb erzeugen
// (fn erhält txDb als this) — sonst kein Rollback möglich.
const createTemplate = db.transaction(async function () {
const txDb = this;
const insertTemplate = txDb.prepare('INSERT INTO templates (name, description, is_assignable, allows_file_upload, ad_create) VALUES (?, ?, ?, ?, ?)');
const insertStep = txDb.prepare('INSERT INTO template_steps (template_id, page_num, label, type, step_order, email_domain, email_source_fields, dropdown_options, ad_field, hidden, ad_prefix) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
const info = await insertTemplate.run(name, description, assignable, fileUpload, adCreate);
const templateId = info.lastInsertRowid;
for (const [idx, step] of steps.entries()) {
await insertStep.run(
templateId, step.page_num || 1, step.label, step.type, idx + 1,
step.email_domain || null, step.email_source_fields || null,
step.dropdown_options || null, step.ad_field || null,
step.hidden ? 1 : 0, step.ad_prefix || null
);
}
return templateId;
});
try {
const templateId = await createTemplate();
auditLog(req.user?.id, 'create_template', 'template', templateId, `Created template: ${name}`, req);
res.status(201).json({ id: templateId, name, description, is_assignable: !!assignable, allows_file_upload: !!fileUpload, ad_create: !!adCreate, steps });
} catch (err) {
res.status(500).json({ error: 'Interner Serverfehler.' });
}
});
// Update template (admin only) - Punkt 8: Transaction
router.put('/:id', adminMiddleware, validate(updateTemplateSchema), async (req, res) => {
const templateId = parseInt(req.params.id);
const { name, description, is_assignable, allows_file_upload, ad_create, steps } = req.validatedBody;
const assignable = is_assignable ? 1 : 0;
const fileUpload = allows_file_upload ? 1 : 0;
const adCreate = ad_create ? 1 : 0;
// H2: Statements innerhalb der Transaktion über txDb erzeugen
const updateTemplateTransaction = db.transaction(async function () {
const txDb = this;
const updateTemplate = txDb.prepare('UPDATE templates SET name = ?, description = ?, is_assignable = ?, allows_file_upload = ?, ad_create = ? WHERE id = ?');
const deleteSteps = txDb.prepare('DELETE FROM template_steps WHERE template_id = ?');
const insertStep = txDb.prepare('INSERT INTO template_steps (template_id, page_num, label, type, step_order, email_domain, email_source_fields, dropdown_options, ad_field, hidden, ad_prefix) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
const info = await updateTemplate.run(name, description, assignable, fileUpload, adCreate, templateId);
if (info.changes === 0) throw new Error('NOT_FOUND');
await deleteSteps.run(templateId);
for (const [idx, step] of steps.entries()) {
await insertStep.run(
templateId, step.page_num || 1, step.label, step.type, idx + 1,
step.email_domain || null, step.email_source_fields || null,
step.dropdown_options || null, step.ad_field || null,
step.hidden ? 1 : 0, step.ad_prefix || null
);
}
});
try {
await updateTemplateTransaction();
auditLog(req.user?.id, 'update_template', 'template', templateId, `Updated template: ${name}`, req);
res.json({ id: templateId, name, description, is_assignable: !!assignable, allows_file_upload: !!fileUpload, ad_create: !!adCreate, steps });
} catch (err) {
if (err.message === 'NOT_FOUND') return res.status(404).json({ error: 'Vorlage nicht gefunden.' });
res.status(500).json({ error: 'Interner Serverfehler.' });
}
});
// Delete template (admin only)
router.delete('/:id', adminMiddleware, async (req, res) => {
const templateId = parseInt(req.params.id);
const info = await db.prepare('DELETE FROM templates WHERE id = ?').run(templateId);
if (info.changes === 0) return res.status(404).json({ error: 'Vorlage nicht gefunden.' });
auditLog(req.user?.id, 'delete_template', 'template', templateId, null, req);
res.json({ message: 'Vorlage gelöscht.' });
});
module.exports = router;