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;