PDF View i Adminbereich
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user