PDF View i Adminbereich

This commit is contained in:
2026-03-18 17:24:13 +01:00
parent a92694f693
commit 8152aab15b
4 changed files with 599 additions and 0 deletions

View File

@@ -1,9 +1,24 @@
// Admin Routes
const bcrypt = require('bcryptjs');
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const { db } = require('../database');
const { requireAdmin } = require('../middleware/auth');
const { testMssqlConnection } = require('../services/mssql-infra-service');
const { getPdfBaseDir } = require('../helpers/pdf-paths');
const YEAR_RE = /^\d{4}$/;
const USER_ID_RE = /^\d+$/;
const PDF_NAME_RE = /^[A-Za-z0-9._-]+\.pdf$/i;
function resolveWithinBaseDir(baseDirResolved, ...parts) {
const targetPath = path.resolve(baseDirResolved, ...parts);
const prefix = baseDirResolved.endsWith(path.sep) ? baseDirResolved : baseDirResolved + path.sep;
if (targetPath !== baseDirResolved && !targetPath.startsWith(prefix)) return null;
return targetPath;
}
// Routes registrieren
function registerAdminRoutes(app) {
@@ -412,6 +427,164 @@ function registerAdminRoutes(app) {
res.json({ success: true });
});
});
// -----------------------
// PDF-Archiv (Admin)
// -----------------------
// Liste vorhandener Jahre unter PDF_BASE_DIR
app.get('/admin/api/pdfs/years', requireAdmin, async (req, res) => {
try {
const baseDirResolved = path.resolve(getPdfBaseDir());
const entries = await fsp.readdir(baseDirResolved, { withFileTypes: true }).catch(() => []);
const years = entries
.filter(d => d.isDirectory() && YEAR_RE.test(d.name))
.map(d => d.name)
.sort((a, b) => Number(b) - Number(a));
res.json({ years });
} catch (err) {
console.error('Fehler beim Laden der PDF-Jahre:', err);
res.status(500).json({ error: 'Fehler beim Laden der PDF-Jahre' });
}
});
// Liste vorhandener User-Ordner für ein Jahr
app.get('/admin/api/pdfs/users', requireAdmin, async (req, res) => {
const year = String(req.query.year || '').trim();
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
try {
const baseDirResolved = path.resolve(getPdfBaseDir());
const yearDir = resolveWithinBaseDir(baseDirResolved, year);
if (!yearDir) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
const entries = await fsp.readdir(yearDir, { withFileTypes: true }).catch(() => []);
const userIds = entries
.filter(d => d.isDirectory() && USER_ID_RE.test(d.name))
.map(d => d.name)
.sort((a, b) => Number(b) - Number(a));
// Zusätzlich Namen aus DB laden, damit der Dropdown menschenlesbar ist.
let usersWithNames = [];
if (userIds.length > 0) {
const placeholders = userIds.map(() => '?').join(',');
const params = userIds.map(id => parseInt(id, 10));
const rows = await new Promise((resolve, reject) => {
db.all(
`SELECT id, firstname, lastname, username FROM users WHERE id IN (${placeholders})`,
params,
(err2, r) => {
if (err2) return reject(err2);
resolve(r || []);
},
);
});
const map = new Map();
(rows || []).forEach(u => {
const name = [u.firstname, u.lastname].filter(Boolean).join(' ').trim();
map.set(String(u.id), name || u.username || String(u.id));
});
usersWithNames = userIds.map(id => ({
id,
name: map.get(String(id)) || String(id),
}));
}
res.json({ userIds, users: usersWithNames });
} catch (err) {
console.error('Fehler beim Laden der PDF-User-Ordner:', err);
res.status(500).json({ error: 'Fehler beim Laden der PDF-User-Ordner' });
}
});
// Liste PDFs für year/userId
app.get('/admin/api/pdfs/files', requireAdmin, async (req, res) => {
const year = String(req.query.year || '').trim();
const userId = String(req.query.userId || '').trim();
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
if (!USER_ID_RE.test(userId)) return res.status(400).json({ error: 'Ungültiger User' });
try {
const baseDirResolved = path.resolve(getPdfBaseDir());
const userDir = resolveWithinBaseDir(baseDirResolved, year, userId);
if (!userDir) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
const entries = await fsp.readdir(userDir, { withFileTypes: true }).catch(() => []);
const pdfEntries = entries.filter(d => d.isFile() && PDF_NAME_RE.test(d.name));
const filesWithMeta = await Promise.all(
pdfEntries.map(async d => {
const stat = await fsp.stat(path.join(userDir, d.name));
return {
name: d.name,
size: stat.size,
mtime: stat.mtime.toISOString()
};
})
);
filesWithMeta.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
res.json({ files: filesWithMeta });
} catch (err) {
console.error('Fehler beim Laden der PDF-Dateiliste:', err);
res.status(500).json({ error: 'Fehler beim Laden der PDF-Dateiliste' });
}
});
// Streamt eine einzelne PDF (Vorschau oder Download)
app.get('/admin/api/pdfs/file', requireAdmin, async (req, res) => {
const year = String(req.query.year || '').trim();
const userId = String(req.query.userId || '').trim();
const nameRaw = String(req.query.name || '');
const inline = String(req.query.inline || 'false') === 'true';
if (!YEAR_RE.test(year)) return res.status(400).json({ error: 'Ungültiges Jahr' });
if (!USER_ID_RE.test(userId)) return res.status(400).json({ error: 'Ungültiger User' });
const safeName = path.basename(nameRaw);
if (!safeName || safeName !== nameRaw || !PDF_NAME_RE.test(safeName)) {
return res.status(400).json({ error: 'Ungültiger Dateiname' });
}
try {
const baseDirResolved = path.resolve(getPdfBaseDir());
const absolutePath = resolveWithinBaseDir(baseDirResolved, year, userId, safeName);
if (!absolutePath) return res.status(400).json({ error: 'Ungültige Pfadanfrage' });
let stat;
try {
stat = await fsp.stat(absolutePath);
} catch (err) {
if (err && err.code === 'ENOENT') return res.status(404).json({ error: 'PDF nicht gefunden' });
throw err;
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Length', stat.size);
if (inline) {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Content-Disposition', `inline; filename="${safeName}"`);
} else {
res.setHeader('Content-Disposition', `attachment; filename="${safeName}"`);
}
const stream = fs.createReadStream(absolutePath);
stream.on('error', (streamErr) => {
console.error('Fehler beim Streamen der PDF:', streamErr);
if (!res.headersSent) res.status(500);
res.end('Fehler beim Lesen der PDF-Datei');
});
return stream.pipe(res);
} catch (err) {
console.error('Fehler beim Streamen der PDF:', err);
if (!res.headersSent) res.status(500).json({ error: 'Fehler beim Streamen der PDF' });
}
});
}
module.exports = registerAdminRoutes;