extended expressjs admin dashboard
This commit is contained in:
@@ -510,6 +510,7 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
|||||||
totalDeletes,
|
totalDeletes,
|
||||||
lastCleanup,
|
lastCleanup,
|
||||||
recentLogs,
|
recentLogs,
|
||||||
|
allUploads,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
get('SELECT COUNT(*) as count FROM uploads'),
|
get('SELECT COUNT(*) as count FROM uploads'),
|
||||||
get('SELECT COALESCE(SUM(size_bytes), 0) as total FROM uploads'),
|
get('SELECT COALESCE(SUM(size_bytes), 0) as total FROM uploads'),
|
||||||
@@ -518,6 +519,7 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
|||||||
get('SELECT COUNT(*) as count FROM admin_logs WHERE event IN (?, ?)', ['delete', 'cleanup']),
|
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']),
|
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 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 = `
|
const stats = `
|
||||||
@@ -540,6 +542,33 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
|||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
const adminUploadsRows = allUploads.map((item) => {
|
||||||
|
const fileUrl = `/_share/${item.stored_name}`;
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${item.owner}</td>
|
||||||
|
<td>
|
||||||
|
<div><strong>${item.original_name}</strong></div>
|
||||||
|
<div class="muted"><a href="${fileUrl}" target="_blank" rel="noopener">${item.stored_name}</a></div>
|
||||||
|
</td>
|
||||||
|
<td>${formatBytes(item.size_bytes)}</td>
|
||||||
|
<td>
|
||||||
|
<div>${formatTimestamp(item.expires_at)}</div>
|
||||||
|
<div class="muted">Noch ${formatCountdown(item.expires_at)}</div>
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form method="post" action="${baseUrl(`/admin/files/${item.id}/delete`)}">
|
||||||
|
<button type="submit" class="secondary">Löschen</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="${baseUrl(`/admin/files/${item.id}/extend`)}">
|
||||||
|
<input name="extendHours" placeholder="Stunden hinzufügen" />
|
||||||
|
<button type="submit">Verlängern</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
const body = `
|
const body = `
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
@@ -570,10 +599,65 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Aktive Uploads</h2>
|
||||||
|
${allUploads.length ? `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nutzer</th>
|
||||||
|
<th>Datei</th>
|
||||||
|
<th>Größe</th>
|
||||||
|
<th>Läuft ab</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${adminUploadsRows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : '<div class="muted">Keine aktiven Uploads.</div>'}
|
||||||
|
</section>
|
||||||
`;
|
`;
|
||||||
res.send(renderPage('Adminübersicht', body));
|
res.send(renderPage('Adminübersicht', body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', '<p class="card">Upload nicht gefunden.</p>'));
|
||||||
|
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', '<p class="card">Upload nicht gefunden.</p>'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const override = parseFloat(req.body.extendHours || '');
|
||||||
|
const extensionSeconds = Number.isFinite(override) && override > 0
|
||||||
|
? Math.round(override * 3600)
|
||||||
|
: uploadTtlSeconds;
|
||||||
|
|
||||||
|
const base = Math.max(uploadEntry.expires_at, Date.now());
|
||||||
|
const nextExpiry = base + extensionSeconds * 1000;
|
||||||
|
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) => {
|
app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => {
|
||||||
const uploads = await all(
|
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',
|
'SELECT id, original_name, stored_name, size_bytes, uploaded_at, expires_at FROM uploads WHERE owner = ? ORDER BY uploaded_at DESC',
|
||||||
|
|||||||
Reference in New Issue
Block a user