'use strict'; const express = require('express'); const config = require('../config'); const auth = require('./auth'); const settings = require('./settings'); const mime = require('./mime'); const utils = require('./utils'); const { pipeArchive } = require('./archive'); /** * Admin area: manage the uploaded files and the settings (name, logo). * Mounted at admin, only available when adminPass is set. */ module.exports = function createAdmin({ db, store, renderPage }) { const router = express.Router(); // not configured: there is no admin area router.use((req, res, next) => config.adminPass ? next() : next('router')); router.get('/', (req, res) => { res.send(renderPage(req, 'admin')); }); const guard = (req, res, next) => auth.isAdmin(req) ? next() : auth.deny(req, res); function fileInfo(sid, file) { const m = file.metadata; const oneTime = m.retention === 'one-time'; return { key: file.key, name: m.name, comment: m.comment || '', size: file.isPartial ? +m.uploadLength : file.size, // unfinished uploads, they use disk space as well partial: !!file.isPartial, retention: m.retention, createdAt: +m.createdAt, lastDownload: m.lastDownload ? +m.lastDownload : null, hasPassword: !!m.password, internalBypass: m.internalBypass === 'true', expiresAt: db.expiresAt(file), // previews of one-time files would use them up previewType: oneTime || file.isPartial ? null : mime.previewKind(m.name), url: `${ config.baseUrl }files/${ sid }++${ file.key }`, thumbUrl: m.thumb && !oneTime ? `${ config.baseUrl }files/${ sid }++${ file.key }.thumb` : null, }; } router.get('/data.json', guard, (req, res) => { const buckets = Object.keys(db.db) .map(sid => ({ sid, files: db.db[sid].map(file => fileInfo(sid, file)) })) .filter(bucket => bucket.files.length > 0); res.set('Cache-Control', 'no-store'); res.json({ buckets, now: Date.now(), totalSize: buckets.reduce((sum, b) => sum + b.files.reduce((s, f) => s + (f.size || 0), 0), 0), }); }); // Browsers can not send a header with a plain download link, so the page asks // for a link which is valid for one minute. router.post('/download-link', guard, (req, res) => { const { sid, key, format } = req.body || {}; if (!utils.isSafeBucketFid(sid)) return res.status(400).json({ message: 'Invalid bucket id' }); const files = (db.get(sid) || []).filter(file => !file.isPartial); if (!files.length) return res.status(404).json({ message: 'Bucket not found' }); if (key) { if (!files.some(file => file.key === key)) return res.status(404).json({ message: 'File not found' }); } else if (!['zip', 'tar.gz'].includes(format)) { return res.status(400).json({ message: 'Invalid format' }); } const { token } = auth.issue({ sid, key: key || null, format: key ? null : format }, 60, 'admin-download'); res.json({ url: `${ config.baseUrl }admin/dl?t=${ encodeURIComponent(token) }` }); }); // Downloads of the admin neither consume one-time files nor count as download router.get('/dl', (req, res) => { const data = auth.verify(String(req.query.t || ''), 'admin-download'); if (!data) return res.status(403).send('Link expired'); const files = (db.get(data.sid) || []).filter(file => !file.isPartial); res.set('Cache-Control', 'no-store, no-transform'); if (data.key) { const file = files.find(f => f.key === data.key); if (!file) return res.status(404).send('File not found'); res.set('Content-Disposition', utils.contentDisposition(file.metadata.name, file.key)); return res.sendFile(store.getFilename(`${ data.sid }++${ file.key }`), err => { if (err && !res.headersSent) res.status(404).end(); }); } if (!files.length) return res.status(404).send('Bucket not found'); res.set('Content-Disposition', utils.contentDisposition(`${ data.sid }.${ data.format }`, 'files')); return pipeArchive(res, files, data.format, store); }); router.delete('/buckets/:sid', guard, async (req, res, next) => { try { if (!utils.isSafeBucketFid(req.params.sid)) return res.status(400).json({ message: 'Invalid bucket id' }); const removed = await db.removeBucket(req.params.sid); if (!removed) return res.status(404).json({ message: 'Bucket not found' }); res.json({ removed }); } catch (e) { next(e); } }); router.delete('/buckets/:sid/files/:key', guard, async (req, res, next) => { try { const { sid, key } = req.params; if (!utils.isSafeBucketFid(sid) || !utils.UUID_V4_RE.test(key)) { return res.status(400).json({ message: 'Invalid id' }); } if (!(db.get(sid) || []).some(file => file.key === key)) { return res.status(404).json({ message: 'File not found' }); } await db.remove(sid, key); res.json({ removed: 1 }); } catch (e) { next(e); } }); // Settings: name, logo, footer router.get('/settings', guard, (req, res) => { res.set('Cache-Control', 'no-store'); res.json({ brand: settings.brand(), defaults: settings.defaults(), maxLogoSize: settings.MAX_LOGO_SIZE }); }); router.put('/settings', guard, (req, res, next) => { try { res.json({ brand: settings.update(req.body || {}) }); } catch (e) { next(e); } }); router.post('/settings/logo', guard, express.raw({ type: () => true, limit: settings.MAX_LOGO_SIZE }), (req, res, next) => { try { res.json({ brand: settings.setLogo(req.body) }); } catch (e) { next(e); } } ); router.delete('/settings/logo', guard, (req, res, next) => { try { res.json({ brand: settings.clearLogo() }); } catch (e) { next(e); } }); router.use((err, req, res, next) => { if (res.headersSent) return next(err); const status = err.status || err.statusCode || 500; if (status >= 500) console.error(err); res.status(status).json({ message: status >= 500 ? 'Internal error' : err.message }); }); return router; };