diff --git a/expressjs/src/server.js b/expressjs/src/server.js index 60c8378..5d14ce4 100644 --- a/expressjs/src/server.js +++ b/expressjs/src/server.js @@ -224,6 +224,49 @@ function formatCountdown(ts) { return `${minutes}m`; } +function renderFileManagerPage(title, body) { + return ` + + + + + ${title} + + + +
+ ${body} +
+ +`; +} + function renderPage(title, body, mainClass = '') { return ` @@ -307,6 +350,23 @@ function requireAdminPage(req, res, next) { 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) { @@ -577,9 +637,12 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {

Adminübersicht

Systemstatistiken und Logs
-
- -
+
+ Dateimanager +
+ +
+

Statistiken

@@ -625,6 +688,218 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { res.send(renderPage('Adminübersicht', body, 'wide')); }); +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 dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== '_share'); + const files = entries.filter((entry) => !entry.isDirectory() && entry.name !== '_share'); + + const rowForEntry = (entry, isDir) => { + const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`); + return ` + + + + ${isDir ? '[DIR]' : '[FILE]'} + ${isDir ? `${entry.name}` : entry.name} + + + ${isDir ? 'Ordner' : 'Datei'} + +
+ + + +
+
+ + +
+ + + `; + }; + + const tableRows = [ + ...dirs.map((entry) => rowForEntry(entry, true)), + ...files.map((entry) => rowForEntry(entry, false)), + ].join(''); + + const body = ` +
+
+

Admin-Dateimanager

+
Verwalten aller Dateien (außer _share).
+
+
+ Zur Adminübersicht +
+ +
+
+
+ +
+
+ Pfad: /${relativePath || ''} + ${relativePath ? `← Zurück` : ''} +
+
+ +
+
+

Ordner erstellen

+
+ + + +
+
+
+

Datei hochladen

+
+ + + +
+
+
+ +
+

Inhalt

+ ${tableRows ? ` + + + + + + + + + + ${tableRows} + +
NameTypAktionen
+ ` : '
Keine Eintraege in diesem Ordner.
'} +
+ `; + + 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/:id/delete`, requireAdminPage, async (req, res) => { const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ?', [req.params.id]); if (!uploadEntry) {