const path = require('path'); const Joi = require('joi'); const winston = require('winston'); const archiver = require('archiver'); const fileExplorer = require('../util/file-explorer'); const admin = require('../util/admin'); const config = require('../state/config'); const dbQueue = require('../db/task-queue'); const transcode = require('./transcode'); const db = require('../db/manager'); const { joiValidate } = require('../util/validation'); const { getTransAlgos, getTransCodecs, getTransBitrates } = require('../api/transcode'); exports.setup = (mstream) => { mstream.all('/api/v1/admin/*', (req, res, next) => { if (config.program.lockAdmin === true) { return res.status(405).json({ error: 'Admin API Disabled' }); } if (req.user.admin !== true) { return res.status(405).json({ error: 'Admin API Disabled' }); } next(); }); mstream.post('/api/v1/admin/lock-api', async (req, res) => { const schema = Joi.object({ lock: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.lockAdminApi(req.body.lock); res.json({}); }); // The admin file explorer can view the entire system mstream.post("/api/v1/admin/file-explorer", async (req, res) => { const schema = Joi.object({ directory: Joi.string().required(), joinDirectory: Joi.string().optional() }); joiValidate(schema, req.body); // Handle home directory let thisDirectory = req.body.directory; if (req.body.directory === '~') { thisDirectory = require('os').homedir(); } if (req.body.joinDirectory) { thisDirectory = path.join(thisDirectory, req.body.joinDirectory); } const folderContents = await fileExplorer.getDirectoryContents(thisDirectory, {}, true); res.json({ path: thisDirectory, directories: folderContents.directories, files: folderContents.files }); }); mstream.get("/api/v1/admin/directories", (req, res) => { res.json(config.program.folders); }); mstream.get("/api/v1/admin/db/params", (req, res) => { res.json(config.program.scanOptions); }); mstream.post("/api/v1/admin/db/params/scan-interval", async (req, res) => { const schema = Joi.object({ scanInterval: Joi.number().integer().min(0).required() }); joiValidate(schema, req.body); await admin.editScanInterval(req.body.scanInterval); res.json({}); }); mstream.post("/api/v1/admin/db/params/save-interval", async (req, res) => { const schema = Joi.object({ saveInterval: Joi.number().integer().min(0).required() }); joiValidate(schema, req.body); await admin.editSaveInterval(req.body.saveInterval); res.json({}); }); mstream.post("/api/v1/admin/db/params/skip-img", async (req, res) => { const schema = Joi.object({ skipImg: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.editSkipImg(req.body.skipImg); res.json({}); }); mstream.post("/api/v1/admin/db/params/pause", async (req, res) => { const schema = Joi.object({ pause: Joi.number().integer().min(0).required() }); joiValidate(schema, req.body); await admin.editPause(req.body.pause); res.json({}); }); mstream.post("/api/v1/admin/db/params/boot-scan-delay", async (req, res) => { const schema = Joi.object({ bootScanDelay: Joi.number().integer().min(0).required() }); joiValidate(schema, req.body); await admin.editBootScanDelay(req.body.bootScanDelay); res.json({}); }); mstream.post("/api/v1/admin/db/params/max-concurrent-scans", async (req, res) => { const schema = Joi.object({ maxConcurrentTasks: Joi.number().integer().min(0).required() }); joiValidate(schema, req.body); await admin.editMaxConcurrentTasks(req.body.maxConcurrentTasks); res.json({}); }); mstream.get("/api/v1/admin/users", (req, res) => { // Scrub passwords const memClone = JSON.parse(JSON.stringify(config.program.users)); Object.keys(memClone).forEach(key => { if(key === 'password' || key === 'salt') { delete memClone[key]; } }); res.json(memClone); }); mstream.put("/api/v1/admin/directory", async (req, res) => { const schema = Joi.object({ directory: Joi.string().required(), vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).required(), autoAccess: Joi.boolean().default(false) }); const input = joiValidate(schema, req.body); await admin.addDirectory(input.value.directory, input.value.vpath, input.value.autoAccess, mstream); res.json({}); try { dbQueue.scanVPath(input.value.vpath); }catch (err) { winston.error('/api/v1/admin/directory failed to add ', { stack: err }); } }); mstream.delete("/api/v1/admin/directory", async (req, res) => { const schema = Joi.object({ vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).required() }); joiValidate(schema, req.body); await admin.removeDirectory(req.body.vpath); res.json({}); }); mstream.put("/api/v1/admin/users", async (req, res) => { const schema = Joi.object({ username: Joi.string().required(), password: Joi.string().required(), vpaths: Joi.array().items(Joi.string()).required(), admin: Joi.boolean().optional().default(false) }); const input = joiValidate(schema, req.body); await admin.addUser( input.value.username, input.value.password, input.value.admin, input.value.vpaths ); res.json({}); }); mstream.post("/api/v1/admin/db/scan/all", (req, res) => { dbQueue.scanAll(); res.json({}); }); mstream.get("/api/v1/admin/db/scan/stats", (req, res) => { let total = 0; if (db.getFileCollection()) { for (const vpath of Object.keys(config.program.folders)) { total += db.getFileCollection().count({ 'vpath': vpath }) } } res.json({ fileCount: total }); }); mstream.delete("/api/v1/admin/users", async (req, res) => { const schema = Joi.object({ username: Joi.string().required() }); joiValidate(schema, req.body); await admin.deleteUser(req.body.username); res.json({}); }); mstream.post("/api/v1/admin/users/password", async (req, res) => { const schema = Joi.object({ username: Joi.string().required(), password: Joi.string().required() }); joiValidate(schema, req.body); await admin.editUserPassword(req.body.username, req.body.password); res.json({}); }); mstream.post("/api/v1/admin/users/lastfm", async (req, res) => { const schema = Joi.object({ username: Joi.string().required(), lasftfmUser: Joi.string().required(), lasftfmPassword: Joi.string().required() }); joiValidate(schema, req.body); await admin.setUserLastFM(req.body.username, req.body.password); res.json({}); }); mstream.post("/api/v1/admin/users/vpaths", async (req, res) => { const schema = Joi.object({ username: Joi.string().required(), vpaths: Joi.array().items(Joi.string()).required() }); joiValidate(schema, req.body); await admin.editUserVPaths(req.body.username, req.body.vpaths); res.json({}); }); mstream.post("/api/v1/admin/users/access", async (req, res) => { const schema = Joi.object({ username: Joi.string().required(), admin: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.editUserAccess(req.body.username, req.body.admin); res.json({}); }); mstream.get("/api/v1/admin/config", (req, res) => { res.json({ address: config.program.address, port: config.program.port, noUpload: config.program.noUpload, writeLogs: config.program.writeLogs, secret: config.program.secret.slice(-4), ssl: config.program.ssl, storage: config.program.storage }); }); mstream.post("/api/v1/admin/config/port", async (req, res) => { const schema = Joi.object({ port: Joi.number().required() }); joiValidate(schema, req.body); await admin.editPort(req.body.port); res.json({}); }); mstream.post("/api/v1/admin/config/address", async (req, res) => { const schema = Joi.object({ address: Joi.string().ip({ cidr: 'forbidden' }).required(), }); joiValidate(schema, req.body); await admin.editAddress(req.body.address); res.json({}); }); mstream.post("/api/v1/admin/config/noupload", async (req, res) => { const schema = Joi.object({ noUpload: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.editUpload(req.body.noUpload); res.json({}); }); mstream.post("/api/v1/admin/config/write-logs", async (req, res) => { const schema = Joi.object({ writeLogs: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.editWriteLogs(req.body.writeLogs); res.json({}); }); mstream.post("/api/v1/admin/config/secret", async (req, res) => { const schema = Joi.object({ strength: Joi.number().integer().positive().required() }); joiValidate(schema, req.body); const secret = await config.asyncRandom(req.body.strength); await admin.editSecret(secret); res.json({}); }); mstream.get("/api/v1/admin/transcode", (req, res) => { const memClone = JSON.parse(JSON.stringify(config.program.transcode)); memClone.downloaded = transcode.isDownloaded(); res.json(memClone); }); mstream.post("/api/v1/admin/transcode/enable", async (req, res) => { const schema = Joi.object({ enable: Joi.boolean().required() }); joiValidate(schema, req.body); await admin.enableTranscode(req.body.enable); res.json({}); }); mstream.post("/api/v1/admin/transcode/default-codec", async (req, res) => { const schema = Joi.object({ defaultCodec: Joi.string().valid(...getTransCodecs()).required() }); joiValidate(schema, req.body); await admin.editDefaultCodec(req.body.defaultCodec); res.json({}); }); mstream.post("/api/v1/admin/transcode/default-bitrate", async (req, res) => { const schema = Joi.object({ defaultBitrate: Joi.string().valid(...getTransBitrates()).required() }); joiValidate(schema, req.body); await admin.editDefaultBitrate(req.body.defaultBitrate); res.json({}); }); mstream.post("/api/v1/admin/transcode/default-algorithm", async (req, res) => { const schema = Joi.object({ algorithm: Joi.string().valid(...getTransAlgos()).required() }); joiValidate(schema, req.body); await admin.editDefaultAlgorithm(req.body.algorithm); res.json({}); }); mstream.post("/api/v1/admin/transcode/download", async (req, res) => { await transcode.downloadedFFmpeg(); res.json({}); }); mstream.get("/api/v1/admin/logs/download", async (req, res) => { const archive = archiver('zip'); archive.on('error', err => { winston.error('Download Error', { stack: err }); res.status(500).json({ error: err.message }); }); res.attachment(`mstream-logs.zip`); //streaming magic archive.pipe(res); archive.directory(config.program.storage.logsDirectory, false) archive.finalize(); }); mstream.get("/api/v1/admin/db/shared", (req, res) => { res.json(db.getShareCollection().find()); }); mstream.delete("/api/v1/admin/db/shared", (req, res) => { const schema = Joi.object({ id: Joi.string().required() }); joiValidate(schema, req.body); db.getShareCollection().findAndRemove({ 'playlistId': { '$eq': req.body.id } }); db.saveShareDB(); res.json({}); }); mstream.delete("/api/v1/admin/db/shared/expired", (req, res) => { db.getShareCollection().findAndRemove({ 'expires': { '$lt': Math.floor(Date.now() / 1000) } }); db.saveShareDB(); res.json({}); }); mstream.delete("/api/v1/admin/db/shared/eternal", (req, res) => { db.getShareCollection().findAndRemove({ 'expires': { '$eq': null } }); db.getShareCollection().findAndRemove({ 'expires': { '$exists': false } }); db.saveShareDB(); res.json({}); }); let enableFederationDebouncer = false; mstream.post('/api/v1/admin/federation/enable', async (req, res) => { const schema = Joi.object({ enable: Joi.boolean().required() }); joiValidate(schema, req.body); if (enableFederationDebouncer === true) { throw new Error('Debouncer Enabled'); } await admin.enableFederation(req.body.enable); enableFederationDebouncer = true; setTimeout(() => { enableFederationDebouncer = false; }, 5000); res.json({}); }); mstream.delete("/api/v1/admin/ssl", async (req, res) => { if (!config.program.ssl.cert) { throw new Error('No Certs'); } await admin.removeSSL(); res.json({}); }); mstream.post("/api/v1/admin/ssl", async (req, res) => { const schema = Joi.object({ cert: Joi.string().required(), key: Joi.string().required() }); joiValidate(schema, req.body); await admin.setSSL(path.resolve(req.body.cert), path.resolve(req.body.key)); res.json({}); }); }