const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const bcrypt = require('bcryptjs'); const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); const express = require('express'); const jwt = require('jsonwebtoken'); const multer = require('multer'); const sqlite3 = require('sqlite3').verbose(); dotenv.config({ path: path.join(__dirname, '..', '..', '.env') }); const basePath = (process.env.BASE_PATH || '/manage').replace(/\/+$/, '') || '/manage'; const port = parseInt(process.env.PORT || '3000', 10); const dataDir = process.env.DATA_DIR || path.join(__dirname, '..', 'data'); const dbPath = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'uploads.sqlite'); const adminHash = process.env.MANAGEMENT_ADMIN_HASH || ''; const uploadTtlSeconds = parseInt(process.env.UPLOAD_TTL_SECONDS || '604800', 10); const maxRetentionSeconds = 90 * 24 * 60 * 60; const maxUploadBytes = parseInt(process.env.UPLOAD_MAX_BYTES || '0', 10); const shareDir = path.join(dataDir, '_share'); const jwtSecret = crypto.randomBytes(32).toString('hex'); const jwtMaxAgeMs = 2 * 60 * 60 * 1000; fs.mkdirSync(shareDir, { recursive: true }); const tempDir = path.join(os.tmpdir(), 'uploads'); fs.mkdirSync(tempDir, { recursive: true }); const upload = multer({ dest: tempDir, limits: maxUploadBytes > 0 ? { fileSize: maxUploadBytes } : undefined, }); const app = express(); const trustProxy = process.env.TRUST_PROXY === 'true'; app.set('trust proxy', trustProxy); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); next(); }); const db = new sqlite3.Database(dbPath); db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS uploads ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner TEXT NOT NULL, original_name TEXT NOT NULL, stored_name TEXT NOT NULL, stored_path TEXT NOT NULL, size_bytes INTEGER NOT NULL, uploaded_at INTEGER NOT NULL, expires_at INTEGER NOT NULL )`); db.run('CREATE INDEX IF NOT EXISTS uploads_owner_idx ON uploads(owner)'); db.run('CREATE INDEX IF NOT EXISTS uploads_expires_idx ON uploads(expires_at)'); db.run(`CREATE TABLE IF NOT EXISTS admin_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL, owner TEXT, detail TEXT, created_at INTEGER NOT NULL )`); db.run('ALTER TABLE uploads ADD COLUMN downloads INTEGER DEFAULT 0', (err) => { /* ignore if column exists */ }); db.run('CREATE INDEX IF NOT EXISTS admin_logs_event_idx ON admin_logs(event)'); db.run('CREATE INDEX IF NOT EXISTS admin_logs_created_idx ON admin_logs(created_at)'); db.run(`CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, password_hash TEXT NOT NULL, created_at INTEGER NOT NULL )`); }); function run(sql, params = []) { return new Promise((resolve, reject) => { db.run(sql, params, function (err) { if (err) { reject(err); return; } resolve(this); }); }); } function get(sql, params = []) { return new Promise((resolve, reject) => { db.get(sql, params, (err, row) => { if (err) { reject(err); return; } resolve(row); }); }); } function all(sql, params = []) { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) { reject(err); return; } resolve(rows); }); }); } function logEvent(event, owner, detail) { const payload = typeof detail === 'string' ? detail : JSON.stringify(detail || {}); return run( 'INSERT INTO admin_logs (event, owner, detail, created_at) VALUES (?, ?, ?, ?)', [event, owner || null, payload, Date.now()] ).catch(() => undefined); } const csrfCookieName = 'csrf'; const csrfCookieOptions = { httpOnly: true, sameSite: 'strict', secure: process.env.COOKIE_SECURE === 'true', }; function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function csrfField(token) { return ``; } function ensureCsrfToken(req, res, next) { let token = req.cookies[csrfCookieName]; if (!token) { token = crypto.randomBytes(32).toString('hex'); res.cookie(csrfCookieName, token, csrfCookieOptions); } res.locals.csrfToken = token; next(); } function isSameOrigin(req) { const origin = req.get('origin'); const referer = req.get('referer'); const header = origin || referer; if (!header) { return true; } try { const parsed = new URL(header); const expected = `${req.protocol}://${req.get('host')}`; return parsed.origin === expected; } catch (err) { return false; } } function csrfGuard(req, res, next) { if (!isSameOrigin(req)) { if (req.path.startsWith(`${basePath}/api/`)) { res.status(403).json({ error: 'Origin check failed' }); return; } res.status(403).send(renderPage('Zugriff verweigert', '

Origin-Prüfung fehlgeschlagen.

')); return; } const token = req.cookies[csrfCookieName]; const provided = req.body?.csrfToken || req.query?.csrfToken || req.get('x-csrf-token'); if (!token || !provided || token !== provided) { if (req.path.startsWith(`${basePath}/api/`)) { res.status(403).json({ error: 'CSRF token mismatch' }); return; } res.status(403).send(renderPage('Zugriff verweigert', '

CSRF-Prüfung fehlgeschlagen.

')); return; } next(); } const loginAttempts = new Map(); const LOGIN_WINDOW_MS = 15 * 60 * 1000; const LOGIN_MAX_ATTEMPTS = 10; function loginRateLimit(type) { return (req, res, next) => { const ip = req.ip || 'unknown'; const key = `${type}:${ip}`; const now = Date.now(); const entry = loginAttempts.get(key) || { count: 0, resetAt: now + LOGIN_WINDOW_MS }; if (now > entry.resetAt) { entry.count = 0; entry.resetAt = now + LOGIN_WINDOW_MS; } entry.count += 1; loginAttempts.set(key, entry); if (entry.count > LOGIN_MAX_ATTEMPTS) { const waitMinutes = Math.ceil((entry.resetAt - now) / 60000); const body = `

Zu viele Anmeldeversuche. Bitte in ${waitMinutes} Minuten erneut versuchen.

`; res.status(429).send(renderPage('Zu viele Versuche', body)); return; } next(); }; } function clearLoginAttempts(type, req) { const ip = req.ip || 'unknown'; loginAttempts.delete(`${type}:${ip}`); } async function getUserHash(username) { const row = await get('SELECT password_hash FROM users WHERE username = ?', [username]); return row ? row.password_hash : null; } function toBase32(buffer) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; let output = ''; for (const byte of buffer) { value = (value << 8) | byte; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; } function createRandomId() { return toBase32(crypto.randomBytes(5)); } function sanitizeBaseName(originalName) { const ext = path.extname(originalName || ''); const base = path.basename(originalName || 'datei', ext); // Allow more characters but keep it safe for URLs and FS const cleaned = base .replace(/[^\w\-. ]/g, '') // Allow words, dashes, dots, spaces .trim() .replace(/\s+/g, '-'); // Replace spaces with dashes for better URLs return cleaned || 'datei'; } function sanitizeExtension(originalName) { const ext = path.extname(originalName || '').toLowerCase(); if (!ext) { return ''; } if (!/^\.[a-z0-9]{1,10}$/.test(ext)) { return ''; } return ext; } function formatBytes(bytes) { if (bytes < 1024) { return `${bytes} B`; } const units = ['KB', 'MB', 'GB', 'TB']; let value = bytes / 1024; let idx = 0; while (value >= 1024 && idx < units.length - 1) { value /= 1024; idx += 1; } return `${value.toFixed(value < 10 ? 1 : 0)} ${units[idx]}`; } function formatTimestamp(ts) { const date = new Date(ts); return date.toLocaleString(); } function formatCountdown(ts) { const delta = Math.max(0, ts - Date.now()); const minutes = Math.floor(delta / 60000); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } if (hours > 0) { return `${hours}h ${minutes % 60}m`; } return `${minutes}m`; } function renderFileManagerPage(title, body) { return ` ${title}
${body}
`; } function renderPage(title, body, mainClass = '') { return ` ${title}
${body}
`; } function baseUrl(pathname) { return `${basePath}${pathname}`; } function getUserFromRequest(req) { const token = req.cookies?.auth; if (!token) { return null; } try { const payload = jwt.verify(token, jwtSecret); if (!payload?.sub) { return null; } return { username: payload.sub, admin: Boolean(payload.admin) }; } catch (err) { return null; } } function requireAuthPage(req, res, next) { const user = getUserFromRequest(req); if (!user) { res.clearCookie('auth'); res.redirect(baseUrl('/login')); return; } req.user = user; next(); } function requireAdminPage(req, res, next) { const user = getUserFromRequest(req); if (!user || !user.admin) { res.clearCookie('auth'); res.redirect(baseUrl('/admin')); return; } req.user = user; next(); } function isAllowedAdminPath(relativePath) { const parts = relativePath.split('/').filter(Boolean); return !parts.includes('_share'); } function resolveAdminPath(relativePath) { const cleaned = relativePath.replace(/\\/g, '/'); if (!isAllowedAdminPath(cleaned)) { return null; } const target = path.resolve(dataDir, cleaned); if (target === dataDir || target.startsWith(`${dataDir}${path.sep}`)) { return target; } return null; } function requireAuthApi(req, res, next) { const user = getUserFromRequest(req); if (!user) { res.clearCookie('auth'); res.status(401).json({ error: 'Unauthorized' }); return; } req.user = user; next(); } async function cleanupExpired() { const now = Date.now(); const expired = await all('SELECT id, stored_path FROM uploads WHERE expires_at <= ?', [now]); let removed = 0; for (const entry of expired) { try { await fs.promises.unlink(entry.stored_path); } catch (err) { // File might already be gone. } await run('DELETE FROM uploads WHERE id = ?', [entry.id]); removed += 1; } if (removed > 0) { await logEvent('cleanup', null, { removed }); } } app.use(ensureCsrfToken); app.use((req, res, next) => { if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { csrfGuard(req, res, next); return; } next(); }); const cleanupTimer = setInterval(() => { cleanupExpired().catch(() => undefined); }, 60 * 1000); cleanupExpired().catch(() => undefined); app.get(`${basePath}/`, (req, res) => { const user = getUserFromRequest(req); if (user) { res.redirect(baseUrl('/dashboard')); return; } res.redirect(baseUrl('/login')); }); app.get(`${basePath}/login`, (req, res) => { const user = getUserFromRequest(req); if (user) { res.redirect(baseUrl('/dashboard')); return; } const body = `

Dateiverwaltung

Bitte anmelden, um Uploads zu verwalten.
${csrfField(res.locals.csrfToken)}
`; res.send(renderPage('Anmeldung', body)); }); app.post(`${basePath}/login`, loginRateLimit('user'), async (req, res) => { const username = String(req.body.username || '').trim(); const password = String(req.body.password || ''); const hash = await getUserHash(username); if (!hash || !bcrypt.compareSync(password, hash)) { const body = `

Dateiverwaltung

Bitte anmelden, um Uploads zu verwalten.
Anmeldung fehlgeschlagen
${csrfField(res.locals.csrfToken)}
`; res.status(401).send(renderPage('Anmeldung', body)); return; } const token = jwt.sign({ sub: username }, jwtSecret, { expiresIn: '2h' }); res.cookie('auth', token, { httpOnly: true, sameSite: 'lax', maxAge: jwtMaxAgeMs, secure: process.env.COOKIE_SECURE === 'true', }); clearLoginAttempts('user', req); await logEvent('login', username, { ok: true }); res.redirect(baseUrl('/dashboard')); }); app.post(`${basePath}/logout`, (req, res) => { res.clearCookie('auth'); res.redirect(baseUrl('/login')); }); app.get(`${basePath}/admin`, async (req, res) => { if (!adminHash) { res.status(404).send(renderPage('Admin', '

Admin-Zugang ist nicht konfiguriert.

')); return; } const user = getUserFromRequest(req); if (user?.admin) { res.redirect(baseUrl('/admin/dashboard')); return; } const body = `

Adminbereich

Admin-Passwort eingeben.
${csrfField(res.locals.csrfToken)}
`; res.send(renderPage('Admin', body)); }); app.post(`${basePath}/admin/login`, loginRateLimit('admin'), async (req, res) => { if (!adminHash) { res.status(404).send(renderPage('Admin', '

Admin-Zugang ist nicht konfiguriert.

')); return; } const password = String(req.body.password || ''); if (!bcrypt.compareSync(password, adminHash)) { const body = `

Adminbereich

Admin-Passwort eingeben.
Anmeldung fehlgeschlagen
${csrfField(res.locals.csrfToken)}
`; res.status(401).send(renderPage('Admin', body)); return; } const token = jwt.sign({ sub: 'admin', admin: true }, jwtSecret, { expiresIn: '2h' }); res.cookie('auth', token, { httpOnly: true, sameSite: 'lax', maxAge: jwtMaxAgeMs, secure: process.env.COOKIE_SECURE === 'true', }); clearLoginAttempts('admin', req); await logEvent('admin_login', 'admin', { ok: true }); res.redirect(baseUrl('/admin/dashboard')); }); app.post(`${basePath}/admin/logout`, (req, res) => { res.clearCookie('auth'); res.redirect(baseUrl('/admin')); }); app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { const [ activeCount, activeBytes, distinctOwners, totalUploads, totalDeletes, lastCleanup, recentLogs, allUploads, ] = await Promise.all([ get('SELECT COUNT(*) as count FROM uploads'), get('SELECT COALESCE(SUM(size_bytes), 0) as total FROM uploads'), get('SELECT COUNT(DISTINCT owner) as count FROM uploads'), get('SELECT COUNT(*) as count FROM admin_logs WHERE event = ?', ['upload']), get('SELECT COUNT(*) as count FROM admin_logs WHERE event IN (?, ?)', ['delete', 'cleanup']), get('SELECT MAX(created_at) as ts FROM admin_logs WHERE event = ?', ['cleanup']), all('SELECT event, owner, detail, created_at FROM admin_logs ORDER BY created_at DESC LIMIT 500'), all('SELECT id, owner, original_name, stored_name, size_bytes, expires_at FROM uploads ORDER BY uploaded_at DESC'), ]); const stats = `
Aktive Uploads ${activeCount.count}
Aktive Größe ${formatBytes(activeBytes.total)}
Aktive Nutzer ${distinctOwners.count}
Uploads gesamt ${totalUploads.count}
Downloads gesamt ${await get('SELECT SUM(downloads) as count FROM uploads').then(r => r.count || 0)}
Löschungen gesamt ${totalDeletes.count}
Letztes Cleanup ${lastCleanup.ts ? formatTimestamp(lastCleanup.ts) : '—'}
`; const logRows = recentLogs.map((entry) => ` ${formatTimestamp(entry.created_at)} ${escapeHtml(entry.event)} ${escapeHtml(entry.owner || '—')} ${escapeHtml(entry.detail || '')} `).join(''); const adminUploadsRows = allUploads.map((item) => { const fileUrl = `/_share/${item.stored_name}`; const fileHref = encodeURI(fileUrl); return ` ${escapeHtml(item.owner)}
${escapeHtml(item.original_name)}
${escapeHtml(item.stored_name)}
${formatBytes(item.size_bytes)}
${formatTimestamp(item.expires_at)}
Noch ${formatCountdown(item.expires_at)}
${csrfField(res.locals.csrfToken)}
${csrfField(res.locals.csrfToken)}
`; }).join(''); const body = `

Adminübersicht

Systemstatistiken und Logs
Dateimanager Benutzer verwalten
${csrfField(res.locals.csrfToken)}

Statistiken

${stats}

Letzte Ereignisse

${logRows || ''}
Zeit Event Nutzer Details
Keine Logs vorhanden.

Aktuelle Uploads

${uploads.length ? ` ${rows}
Datei Größe Läuft ab Aktionen
` : '
Noch keine Uploads.
'}
`; res.send(renderPage('Adminübersicht', body, 'wide')); }); app.get(`${basePath}/admin/users`, requireAdminPage, async (req, res) => { const users = await all('SELECT username, created_at FROM users ORDER BY username ASC'); const rows = users.map((user) => { const username = escapeHtml(user.username); const encoded = encodeURIComponent(user.username); return ` ${username} ${formatTimestamp(user.created_at)}
${csrfField(res.locals.csrfToken)}
${csrfField(res.locals.csrfToken)}
`; }).join(''); const body = `

Benutzer verwalten

Zugänge im System verwalten.
Zur Adminübersicht
${csrfField(res.locals.csrfToken)}

Neuen Benutzer anlegen

${csrfField(res.locals.csrfToken)}

Benutzerliste

${users.length ? ` ${rows}
Benutzername Erstellt Aktionen
` : '
Keine Benutzer vorhanden.
'}
`; res.send(renderPage('Benutzer verwalten', body, 'wide')); }); app.post(`${basePath}/admin/users/create`, requireAdminPage, async (req, res) => { const username = String(req.body.username || '').trim(); const password = String(req.body.password || ''); if (!username || username.length > 200 || !password) { res.status(400).send(renderPage('Benutzer verwalten', '

Ungültige Eingabe.

', 'wide')); return; } const hash = bcrypt.hashSync(password, 12); try { await run('INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)', [ username, hash, Date.now(), ]); } catch (err) { res.status(400).send(renderPage('Benutzer verwalten', '

Benutzername existiert bereits.

', 'wide')); return; } await logEvent('admin_user_create', 'admin', { username }); res.redirect(baseUrl('/admin/users')); }); app.post(`${basePath}/admin/users/:username/reset`, requireAdminPage, async (req, res) => { const username = decodeURIComponent(req.params.username || ''); const password = String(req.body.password || ''); if (!username || !password) { res.status(400).send(renderPage('Benutzer verwalten', '

Ungültige Eingabe.

', 'wide')); return; } const hash = bcrypt.hashSync(password, 12); await run('UPDATE users SET password_hash = ? WHERE username = ?', [hash, username]); await logEvent('admin_user_reset', 'admin', { username }); res.redirect(baseUrl('/admin/users')); }); app.post(`${basePath}/admin/users/:username/delete`, requireAdminPage, async (req, res) => { const username = decodeURIComponent(req.params.username || ''); if (!username) { res.status(400).send(renderPage('Benutzer verwalten', '

Ungültige Eingabe.

', 'wide')); return; } await run('DELETE FROM users WHERE username = ?', [username]); await logEvent('admin_user_delete', 'admin', { username }); res.redirect(baseUrl('/admin/users')); }); app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => { const relativePath = String(req.query.path || '').replace(/^\/+/, ''); const resolved = resolveAdminPath(relativePath); if (!resolved) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } let entries; try { entries = await fs.promises.readdir(resolved, { withFileTypes: true }); } catch (err) { res.status(500).send(renderFileManagerPage('Admin-Dateien', '

Ordner kann nicht gelesen werden.

')); return; } const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : ''; const filtered = entries.filter((entry) => entry.name !== '_share'); const details = await Promise.all( filtered.map(async (entry) => { const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; let stat = null; try { stat = await fs.promises.stat(path.join(resolved, entry.name)); } catch (err) { stat = null; } return { entry, childPath, isDir: entry.isDirectory(), size: stat && stat.isFile() ? stat.size : null, modifiedAt: stat ? stat.mtimeMs : null, }; }) ); const dirs = details.filter((item) => item.isDir); const files = details.filter((item) => !item.isDir); const rowForEntry = (item) => { const { entry, childPath, isDir, size, modifiedAt } = item; const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`); const escapedName = escapeHtml(entry.name); const escapedPath = escapeHtml(childPath); return ` ${isDir ? 'DIR' : 'FILE'} ${isDir ? `${escapedName}` : escapedName} ${isDir ? 'Ordner' : 'Datei'} ${size ? formatBytes(size) : '—'} ${modifiedAt ? formatTimestamp(modifiedAt) : '—'} `; }; const tableRows = [ ...dirs.map((entry) => rowForEntry(entry)), ...files.map((entry) => rowForEntry(entry)), ].join(''); const body = `

Admin-Dateimanager

Verwalten aller Dateien (außer _share).
Zur Adminübersicht
${csrfField(res.locals.csrfToken)}
Pfad /${escapeHtml(relativePath || '')} ${relativePath ? `← Zurück` : ''}
Position: ${relativePath ? relativePath.split('/').map((segment, idx, parts) => { const crumbPath = parts.slice(0, idx + 1).join('/'); return `${escapeHtml(segment)}`; }).join('/') : 'Root'}

Ordner erstellen

${csrfField(res.locals.csrfToken)}

Datei hochladen

${csrfField(res.locals.csrfToken)}

Inhalt

${tableRows ? ` ${tableRows}
Name Typ Größe Geändert Aktionen
` : '
Keine Eintraege in diesem Ordner.
'}
${csrfField(res.locals.csrfToken)}

Umbenennen

${csrfField(res.locals.csrfToken)}

Löschen

${csrfField(res.locals.csrfToken)}

Verschieben

${csrfField(res.locals.csrfToken)}

Kopieren

`; res.send(renderFileManagerPage('Admin-Dateien', body)); }); app.post(`${basePath}/admin/files/mkdir`, requireAdminPage, async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); const name = String(req.body.name || '').trim(); if (!name || name.includes('/') || name.includes('\\') || name === '_share') { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Ordnername.

')); return; } const base = resolveAdminPath(relativePath); if (!base) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } const target = path.join(base, name); await fs.promises.mkdir(target, { recursive: true }); await logEvent('admin_mkdir', 'admin', { path: path.join(relativePath, name) }); res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(relativePath)}`)); }); app.post(`${basePath}/admin/files/upload`, requireAdminPage, upload.single('file'), async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); const base = resolveAdminPath(relativePath); if (!base) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } if (!req.file) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Keine Datei hochgeladen.

')); return; } const filename = path.basename(req.file.originalname); if (filename === '_share') { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Dateiname.

')); return; } const target = path.join(base, filename); try { await fs.promises.rename(req.file.path, target); } catch (err) { if (err.code === 'EXDEV') { await fs.promises.copyFile(req.file.path, target); await fs.promises.unlink(req.file.path); } else { throw err; } } await logEvent('admin_upload', 'admin', { path: path.join(relativePath, filename) }); res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(relativePath)}`)); }); app.post(`${basePath}/admin/files/rename`, requireAdminPage, async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); const newName = String(req.body.newName || '').trim(); if (!relativePath) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Root kann nicht umbenannt werden.

')); return; } if (!newName || newName.includes('/') || newName.includes('\\') || newName === '_share') { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger neuer Name.

')); return; } const resolved = resolveAdminPath(relativePath); if (!resolved) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } const target = path.join(path.dirname(resolved), newName); await fs.promises.rename(resolved, target); await logEvent('admin_rename', 'admin', { from: relativePath, to: path.join(path.dirname(relativePath), newName) }); const parent = path.dirname(relativePath); const nextPath = parent === '.' ? '' : parent; res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(nextPath)}`)); }); app.post(`${basePath}/admin/files/delete`, requireAdminPage, async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); if (!relativePath) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Root kann nicht gelöscht werden.

')); return; } const resolved = resolveAdminPath(relativePath); if (!resolved) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } await fs.promises.rm(resolved, { recursive: true, force: true }); await logEvent('admin_delete', 'admin', { path: relativePath }); const parent = path.dirname(relativePath); const nextPath = parent === '.' ? '' : parent; res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(nextPath)}`)); }); app.post(`${basePath}/admin/files/move`, requireAdminPage, async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); const targetPath = String(req.body.targetPath || '').replace(/^\/+/, ''); if (!relativePath || !targetPath) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültige Eingabe.

')); return; } const source = resolveAdminPath(relativePath); const targetBase = resolveAdminPath(targetPath); if (!source || !targetBase) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } let target = targetBase; try { const stat = await fs.promises.stat(targetBase); if (stat.isDirectory()) { target = path.join(targetBase, path.basename(source)); } } catch (err) { // targetBase does not exist; treat as file/dir path. } try { await fs.promises.rename(source, target); } catch (err) { if (err.code === 'EXDEV') { await fs.promises.cp(source, target, { recursive: true, force: false }); await fs.promises.rm(source, { recursive: true, force: true }); } else { throw err; } } await logEvent('admin_move', 'admin', { from: relativePath, to: targetPath }); res.redirect(baseUrl('/admin/files')); }); app.post(`${basePath}/admin/files/copy`, requireAdminPage, async (req, res) => { const relativePath = String(req.body.path || '').replace(/^\/+/, ''); const targetPath = String(req.body.targetPath || '').replace(/^\/+/, ''); if (!relativePath || !targetPath) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültige Eingabe.

')); return; } const source = resolveAdminPath(relativePath); const targetBase = resolveAdminPath(targetPath); if (!source || !targetBase) { res.status(400).send(renderFileManagerPage('Admin-Dateien', '

Ungültiger Pfad.

')); return; } let target = targetBase; try { const stat = await fs.promises.stat(targetBase); if (stat.isDirectory()) { target = path.join(targetBase, path.basename(source)); } } catch (err) { // targetBase does not exist; treat as file/dir path. } await fs.promises.cp(source, target, { recursive: true, force: false }); await logEvent('admin_copy', 'admin', { from: relativePath, to: targetPath }); res.redirect(baseUrl('/admin/files')); }); app.post(`${basePath}/admin/files/:id/delete`, requireAdminPage, async (req, res) => { const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ?', [req.params.id]); if (!uploadEntry) { res.status(404).send(renderPage('Nicht gefunden', '

Upload nicht gefunden.

')); return; } try { await fs.promises.unlink(uploadEntry.stored_path); } catch (err) { // Ignore missing files. } await run('DELETE FROM uploads WHERE id = ?', [uploadEntry.id]); await logEvent('delete', 'admin', { id: uploadEntry.id }); res.redirect(baseUrl('/admin/dashboard')); }); app.post(`${basePath}/admin/files/:id/extend`, requireAdminPage, async (req, res) => { const uploadEntry = await get('SELECT id, expires_at FROM uploads WHERE id = ?', [req.params.id]); if (!uploadEntry) { res.status(404).send(renderPage('Nicht gefunden', '

Upload nicht gefunden.

')); return; } const override = parseFloat(req.body.extendHours || ''); const extensionSeconds = Number.isFinite(override) && override > 0 ? Math.round(override * 3600) : uploadTtlSeconds; const now = Date.now(); const base = Math.max(uploadEntry.expires_at, now); const maxExpiry = now + maxRetentionSeconds * 1000; const nextExpiry = Math.min(base + extensionSeconds * 1000, maxExpiry); await run('UPDATE uploads SET expires_at = ? WHERE id = ?', [nextExpiry, uploadEntry.id]); await logEvent('extend', 'admin', { id: uploadEntry.id, expires_at: nextExpiry }); res.redirect(baseUrl('/admin/dashboard')); }); app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => { const uploads = await all( 'SELECT id, original_name, stored_name, size_bytes, uploaded_at, expires_at FROM uploads WHERE owner = ? ORDER BY uploaded_at DESC', [req.user.username] ); const rows = uploads.map((item) => { const fileUrl = `/_share/${item.stored_name}`; const fileHref = encodeURI(fileUrl); return `
${escapeHtml(item.original_name)}
${escapeHtml(item.stored_name)}
${formatBytes(item.size_bytes)}
${formatTimestamp(item.expires_at)}
Noch ${formatCountdown(item.expires_at)}
${csrfField(res.locals.csrfToken)}
${csrfField(res.locals.csrfToken)}
`; }).join(''); const body = `

Dateiverwaltung

Angemeldet als ${escapeHtml(req.user.username)}
${csrfField(res.locals.csrfToken)}

Datei hochladen

${csrfField(res.locals.csrfToken)}

Aktuelle Uploads

${uploads.length ? ` ${rows}
Datei Größe Läuft ab Aktionen
` : '
Noch keine Uploads.
'}
`; res.send(renderPage('Übersicht', body)); }); app.post(`${basePath}/api/upload`, requireAuthApi, upload.single('file'), async (req, res) => { if (!req.file) { res.status(400).json({ error: 'No file uploaded' }); return; } const now = Date.now(); // Don't sanitize rigorously anymore, we rely on content-disposition header for safety // but we still want a safe filename for storage const ext = path.extname(req.file.originalname); const token = createRandomId(); // We keep the original filename in the DB but store it with a safe ID on disk const storedName = `${token}${ext}`; const storedPath = path.join(shareDir, storedName); const retentionOverride = parseFloat(req.body.retentionHours || ''); const retentionSeconds = Number.isFinite(retentionOverride) && retentionOverride > 0 ? Math.round(retentionOverride * 3600) : uploadTtlSeconds; const cappedRetention = Math.min(retentionSeconds, maxRetentionSeconds); try { await fs.promises.rename(req.file.path, storedPath); } catch (err) { if (err.code === 'EXDEV') { await fs.promises.copyFile(req.file.path, storedPath); await fs.promises.unlink(req.file.path); } else { throw err; } } await run( `INSERT INTO uploads (owner, original_name, stored_name, stored_path, size_bytes, uploaded_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ req.user.username, req.file.originalname, // Store exact original name storedName, storedPath, req.file.size, now, now + cappedRetention * 1000, ] ); await logEvent('upload', req.user.username, { name: storedName, size: req.file.size }); res.json({ ok: true, name: storedName }); }); app.post(`${basePath}/files/:id/delete`, requireAuthPage, async (req, res) => { const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ? AND owner = ?', [ req.params.id, req.user.username, ]); if (!uploadEntry) { res.status(404).send(renderPage('Nicht gefunden', '

Upload nicht gefunden.

')); return; } try { await fs.promises.unlink(uploadEntry.stored_path); } catch (err) { // Ignore missing files. } await run('DELETE FROM uploads WHERE id = ?', [uploadEntry.id]); await logEvent('delete', req.user.username, { id: uploadEntry.id }); res.redirect(baseUrl('/dashboard')); }); app.post(`${basePath}/files/:id/extend`, requireAuthPage, async (req, res) => { const uploadEntry = await get('SELECT id, expires_at FROM uploads WHERE id = ? AND owner = ?', [ req.params.id, req.user.username, ]); if (!uploadEntry) { res.status(404).send(renderPage('Nicht gefunden', '

Upload nicht gefunden.

')); return; } const override = parseFloat(req.body.extendHours || ''); const extensionSeconds = Number.isFinite(override) && override > 0 ? Math.round(override * 3600) : uploadTtlSeconds; const now = Date.now(); const base = Math.max(uploadEntry.expires_at, now); const maxExpiry = now + maxRetentionSeconds * 1000; const nextExpiry = Math.min(base + extensionSeconds * 1000, maxExpiry); await run('UPDATE uploads SET expires_at = ? WHERE id = ?', [nextExpiry, uploadEntry.id]); await logEvent('extend', req.user.username, { id: uploadEntry.id, expires_at: nextExpiry }); res.redirect(baseUrl('/dashboard')); }); app.get('/_share/:filename', async (req, res) => { const filename = req.params.filename; // Security check: ensure no path traversal if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) { res.status(400).send('Invalid filename'); return; } const row = await get('SELECT id, original_name, stored_path FROM uploads WHERE stored_name = ?', [filename]); if (!row) { // If not found in DB, check if it exists on disk (legacy or manual files) const filePath = path.join(shareDir, filename); if (fs.existsSync(filePath)) { // Log download for legacy/manual files if needed, or just skip res.download(filePath, filename); // Fallback: download with stored name return; } res.status(404).send('File not found'); return; } // Log download run('UPDATE uploads SET downloads = downloads + 1 WHERE id = ?', [row.id]).catch(() => undefined); logEvent('download', null, { name: filename, original: row.original_name }).catch(() => undefined); res.download(row.stored_path, row.original_name); }); app.use((req, res) => { res.status(404).send(renderPage('Nicht gefunden', '

Seite nicht gefunden.

')); }); const server = app.listen(port, () => { console.log(`Express server listening on ${port} with base path ${basePath}`); }); function shutdown(signal) { clearInterval(cleanupTimer); server.close(() => { db.close(() => { console.log(`Shutdown complete (${signal}).`); process.exit(0); }); }); setTimeout(() => { console.error('Forced shutdown after timeout.'); process.exit(1); }, 5000).unref(); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));