v1.13: PDF-Upload und -Download fuer Dokumente (multer, Upload-Volume, Rechtepruefung)
This commit is contained in:
+81
-2
@@ -8,6 +8,9 @@
|
||||
|
||||
const express = require('express')
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const multer = require('multer')
|
||||
const { pool, init, sessionsAufraeumen } = require('./db')
|
||||
const { seed, seedeZaehler, seedeHaeuser } = require('./seed')
|
||||
const mail = require('./mail')
|
||||
@@ -18,11 +21,40 @@ const COOKIE_NAME = 'mp_session'
|
||||
// KI-Antworten auf Schadensmeldungen über n8n (leer = deaktiviert)
|
||||
const N8N_TICKET_WEBHOOK = process.env.N8N_TICKET_WEBHOOK || ''
|
||||
const KI_API_TOKEN = process.env.KI_API_TOKEN || ''
|
||||
// PDF-Ablage für das Dokumentenmodul (im Container per Volume persistiert)
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads')
|
||||
const UPLOAD_MAX_BYTES = 25 * 1024 * 1024 // 25 MB pro Datei
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
const app = express()
|
||||
app.use(express.json({ limit: '256kb' }))
|
||||
app.set('trust proxy', true)
|
||||
|
||||
// PDF-Uploads (nur application/pdf, 25 MB); der Dateiname wird erst nach dem
|
||||
// Dokument-Lookup vergeben → die Datei heißt wie die Dokument-ID (<id>.pdf).
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: UPLOAD_DIR,
|
||||
filename: (req, _file, cb) => cb(null, `${req.params.id}.pdf`),
|
||||
}),
|
||||
limits: { fileSize: UPLOAD_MAX_BYTES, files: 1 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const istPdf = file.mimetype === 'application/pdf' || /\.pdf$/i.test(file.originalname || '')
|
||||
cb(istPdf ? null : new Error('Nur PDF-Dateien sind erlaubt.'), istPdf)
|
||||
},
|
||||
})
|
||||
|
||||
/** Deutsche Größenangabe, z. B. 1,8 MB. */
|
||||
function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '–'
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
const einheiten = ['KB', 'MB', 'GB']
|
||||
let wert = bytes / 1024
|
||||
let i = 0
|
||||
while (wert >= 1024 && i < einheiten.length - 1) { wert /= 1024; i += 1 }
|
||||
return `${wert.toLocaleString('de-DE', { maximumFractionDigits: 1 })} ${einheiten[i]}`
|
||||
}
|
||||
|
||||
// Prozess nie durch einen einzelnen Fehler killen lassen (z. B. defekter
|
||||
// JSON-Body von außen); Fehler werden geloggt, der Dienst läuft weiter.
|
||||
process.on('uncaughtException', (e) => console.error('[api] uncaughtException:', e))
|
||||
@@ -127,6 +159,7 @@ const mapTicket = (t) => ({
|
||||
})
|
||||
const mapDocument = (d) => ({
|
||||
id: d.id, unitId: d.unit_id, title: d.title, category: d.category, size: d.size, date: d.date,
|
||||
fileName: d.file_name ?? null, hasFile: Boolean(d.file_path),
|
||||
})
|
||||
const mapMessage = (m) => ({ id: m.id, from: m.from_role, author: m.author, text: m.text, at: m.at })
|
||||
const mapThread = (t) => ({
|
||||
@@ -540,12 +573,58 @@ app.patch('/api/documents/:id', auth, adminOnly, async (req, res, next) => {
|
||||
|
||||
app.delete('/api/documents/:id', auth, adminOnly, async (req, res, next) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query('DELETE FROM documents WHERE id = $1', [req.params.id])
|
||||
if (!rowCount) return res.status(404).json({ fehler: 'Dokument nicht gefunden.' })
|
||||
const { rows } = await pool.query('DELETE FROM documents WHERE id = $1 RETURNING file_path', [req.params.id])
|
||||
if (!rows.length) return res.status(404).json({ fehler: 'Dokument nicht gefunden.' })
|
||||
if (rows[0].file_path) {
|
||||
// Datei best-effort mitlöschen (Fehler hier dürfen den Request nicht killen)
|
||||
await fs.promises.unlink(path.join(UPLOAD_DIR, rows[0].file_path)).catch(() => {})
|
||||
}
|
||||
res.json({ ok: true })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// PDF-Upload zum Dokument (ersetzt eine vorhandene Datei; nur Verwaltung)
|
||||
app.post('/api/documents/:id/upload', auth, adminOnly, async (req, res, next) => {
|
||||
const { rows } = await pool.query('SELECT id, file_path FROM documents WHERE id = $1', [req.params.id])
|
||||
if (!rows.length) return res.status(404).json({ fehler: 'Dokument nicht gefunden.' })
|
||||
const alteDatei = rows[0].file_path
|
||||
upload.single('file')(req, res, async (fehler) => {
|
||||
try {
|
||||
if (fehler) {
|
||||
const meldung = fehler.code === 'LIMIT_FILE_SIZE'
|
||||
? 'Die Datei ist größer als 25 MB.'
|
||||
: (fehler.message || 'Der Upload ist fehlgeschlagen.')
|
||||
return res.status(fehler.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({ fehler: meldung })
|
||||
}
|
||||
if (!req.file) return res.status(400).json({ fehler: 'Keine Datei übertragen.' })
|
||||
if (alteDatei && alteDatei !== req.file.filename) {
|
||||
await fs.promises.unlink(path.join(UPLOAD_DIR, alteDatei)).catch(() => {})
|
||||
}
|
||||
const originalname = Buffer.from(req.file.originalname, 'latin1').toString('utf8') // MSIE-/Chrome-Kodierung geraderücken
|
||||
const { rows: aktualisiert } = await pool.query(
|
||||
'UPDATE documents SET file_name = $1, file_path = $2, size = $3 WHERE id = $4 RETURNING *',
|
||||
[originalname, req.file.filename, formatBytes(req.file.size), req.params.id],
|
||||
)
|
||||
res.json({ dokument: mapDocument(aktualisiert[0]) })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
})
|
||||
|
||||
// PDF-Download: Verwaltung immer, Mieter nur bei eigener Wohnung oder Hausdokument
|
||||
app.get('/api/documents/:id/download', auth, async (req, res, next) => {
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT * FROM documents WHERE id = $1', [req.params.id])
|
||||
if (!rows.length) return res.status(404).json({ fehler: 'Dokument nicht gefunden.' })
|
||||
const d = rows[0]
|
||||
const erlaubt = req.user.rolle === 'verwaltung' || d.unit_id === null || d.unit_id === req.user.einheit_id
|
||||
if (!erlaubt) return res.status(403).json({ fehler: 'Kein Zugriff auf dieses Dokument.' })
|
||||
if (!d.file_path) return res.status(404).json({ fehler: 'Zu diesem Eintrag ist noch keine Datei hinterlegt.' })
|
||||
const vollerPfad = path.join(UPLOAD_DIR, path.basename(d.file_path)) // basename: Pfad-Traversal unterbinden
|
||||
if (!fs.existsSync(vollerPfad)) return res.status(404).json({ fehler: 'Datei nicht gefunden.' })
|
||||
res.download(vollerPfad, d.file_name || `${d.title}.pdf`)
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// ── Bestand: Häuser & Wohnungen (Verwaltung) ────────────────
|
||||
function pruefeHaus(b) {
|
||||
const fehler = []
|
||||
|
||||
Reference in New Issue
Block a user