This commit is contained in:
portakal 2026-06-16 14:16:02 +03:00
commit 654eb49502
30 changed files with 4420 additions and 0 deletions

171
src/routes/admin.js Normal file
View file

@ -0,0 +1,171 @@
import express from 'express';
import config from '../config.js';
import { query, queryOne, execute } from '../db.js';
import { deleteStored } from '../storage.js';
import { mintLink, allFilesWithOwner } from '../links.js';
import { getDiskInfo } from '../services/disk.js';
import { queueDepth } from '../services/compression.js';
import { requireAdmin, checkOrigin, setFlash } from '../auth.js';
import { adminOverviewPage, adminUsersPage, adminFilesPage } from '../views.js';
import { ah } from '../util.js';
import log from '../log.js';
const router = express.Router();
router.use(requireAdmin);
router.get(
'/',
ah(async (req, res) => {
const [counts, disk] = await Promise.all([
queryOne(`SELECT
(SELECT COUNT(*) FROM users) AS users,
(SELECT COUNT(*) FROM files) AS files,
(SELECT COUNT(*) FROM download_links WHERE used = 0 AND expires_at > NOW()) AS activeLinks,
(SELECT COUNT(*) FROM download_links WHERE used = 1) AS usedLinks,
(SELECT COUNT(*) FROM download_links WHERE used = 0 AND expires_at <= NOW()) AS expiredLinks,
(SELECT COUNT(*) FROM files WHERE av_status = 'infected') AS infected`),
getDiskInfo(),
]);
res.send(
adminOverviewPage({
user: req.user,
stats: { ...counts, queueDepth: queueDepth() },
disk,
flash: res.locals.flash,
})
);
})
);
router.get(
'/users',
ah(async (req, res) => {
const users = await query(
`SELECT u.id, u.username, u.email, u.is_admin, u.disabled, u.email_verified,
COUNT(f.id) AS file_count, COALESCE(SUM(f.size_bytes), 0) AS bytes_used
FROM users u
LEFT JOIN files f ON f.user_id = u.id
GROUP BY u.id
ORDER BY u.created_at DESC`
);
res.send(adminUsersPage({ user: req.user, users, flash: res.locals.flash }));
})
);
router.get(
'/files',
ah(async (req, res) => {
const files = await allFilesWithOwner();
res.send(adminFilesPage({ user: req.user, files, flash: res.locals.flash }));
})
);
// ---- Mutations ----
router.use(checkOrigin);
router.post(
'/users/:id/verify',
ah(async (req, res) => {
await execute(
'UPDATE users SET email_verified = 1, verify_token = NULL WHERE id = ?',
[req.params.id]
);
audit(req, 'admin.user.verify', { targetUser: req.params.id });
setFlash(req, 'ok', 'User verified.');
res.redirect(config.url('/admin/users'));
})
);
router.post(
'/users/:id/disable',
ah(async (req, res) => {
if (Number(req.params.id) === Number(req.user.id)) {
setFlash(req, 'error', 'You cannot disable your own account.');
return res.redirect(config.url('/admin/users'));
}
await execute('UPDATE users SET disabled = 1 WHERE id = ?', [req.params.id]);
audit(req, 'admin.user.disable', { targetUser: req.params.id });
setFlash(req, 'ok', 'User disabled.');
res.redirect(config.url('/admin/users'));
})
);
router.post(
'/users/:id/enable',
ah(async (req, res) => {
await execute('UPDATE users SET disabled = 0 WHERE id = ?', [req.params.id]);
audit(req, 'admin.user.enable', { targetUser: req.params.id });
setFlash(req, 'ok', 'User enabled.');
res.redirect(config.url('/admin/users'));
})
);
router.post(
'/users/:id/delete',
ah(async (req, res) => {
if (Number(req.params.id) === Number(req.user.id)) {
setFlash(req, 'error', 'You cannot delete your own account.');
return res.redirect(config.url('/admin/users'));
}
const files = await query('SELECT stored_name FROM files WHERE user_id = ?', [
req.params.id,
]);
for (const f of files) await deleteStored(f.stored_name);
await execute('DELETE FROM users WHERE id = ?', [req.params.id]); // cascades files+links
audit(req, 'admin.user.delete', { targetUser: req.params.id, files: files.length });
setFlash(req, 'ok', 'User and their files deleted.');
res.redirect(config.url('/admin/users'));
})
);
router.post(
'/files/:id/delete',
ah(async (req, res) => {
const file = await queryOne('SELECT stored_name FROM files WHERE id = ?', [
req.params.id,
]);
if (file) {
await deleteStored(file.stored_name);
await execute('DELETE FROM files WHERE id = ?', [req.params.id]);
audit(req, 'admin.file.delete', { fileId: req.params.id });
setFlash(req, 'ok', 'File deleted.');
}
res.redirect(config.url('/admin/files'));
})
);
router.post(
'/files/:id/link',
ah(async (req, res) => {
const file = await queryOne('SELECT id, av_status FROM files WHERE id = ?', [
req.params.id,
]);
if (!file) {
setFlash(req, 'error', 'File not found.');
return res.redirect(config.url('/admin/files'));
}
const token = await mintLink(file.id);
audit(req, 'admin.file.link', { fileId: file.id });
setFlash(req, 'ok', `New link: ${config.absoluteUrl(`/d/${token}`)}`);
res.redirect(config.url('/admin/files'));
})
);
router.post(
'/links/:id/expire',
ah(async (req, res) => {
await execute('UPDATE download_links SET expires_at = NOW() WHERE id = ?', [
req.params.id,
]);
audit(req, 'admin.link.expire', { linkId: req.params.id });
setFlash(req, 'ok', 'Link expired.');
res.redirect(config.url('/admin/files'));
})
);
function audit(req, action, meta) {
log.audit(`user#${req.user.id}(${req.user.username})`, action, meta);
}
export default router;