From 999e5d49cd886c83162390e86f2bc01c91af671e Mon Sep 17 00:00:00 2001 From: IrosTheBeggar Date: Mon, 1 Mar 2021 11:21:07 -0500 Subject: [PATCH] UI for managing shared playlists --- src/api/admin.js | 51 +++++++++ src/api/shared.js | 7 +- src/util/admin.js | 3 + webapp/admin/index.js | 220 +++++++++++++++++++++++++++++++++++- webapp/assets/js/mstream.js | 1 + 5 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/api/admin.js b/src/api/admin.js index 417603b..b3c549c 100644 --- a/src/api/admin.js +++ b/src/api/admin.js @@ -7,6 +7,7 @@ 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'); exports.setup = (mstream) => { // The admin file explorer can view the entire system @@ -595,4 +596,54 @@ exports.setup = (mstream) => { archive.directory(config.program.storage.logsDirectory, false) archive.finalize(); }); + + mstream.get("/api/v1/admin/db/shared", async (req, res) => { + try { + res.json(db.getShareCollection().find()); + } catch (err) { + winston.error('admin error', {stack: err}); + res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' }); + } + }); + + mstream.delete("/api/v1/admin/db/shared", async (req, res) => { + try { + const schema = Joi.object({ id: Joi.string().required() }); + await schema.validateAsync(req.body); + }catch (err) { + return res.status(500).json({ error: 'Validation Error' }); + } + + try { + db.getShareCollection().findAndRemove({ 'playlistId': { '$eq': req.body.id } }); + db.saveShareDB(); + res.json({}); + } catch (err) { + winston.error('admin error', {stack: err}); + res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' }); + } + }); + + mstream.delete("/api/v1/admin/db/shared/expired", async (req, res) => { + try { + db.getShareCollection().findAndRemove({ 'expires': { '$lt': Math.floor(Date.now() / 1000) } }); + db.saveShareDB(); + res.json({}); + } catch (err) { + winston.error('admin error', {stack: err}); + res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' }); + } + }); + + mstream.delete("/api/v1/admin/db/shared/eternal", async (req, res) => { + try { + db.getShareCollection().findAndRemove({ 'expires': { '$eq': null } }); + db.getShareCollection().findAndRemove({ 'expires': { '$exists': false } }); + db.saveShareDB(); + res.json({}); + } catch (err) { + winston.error('admin error', {stack: err}); + res.status(500).json({ error: typeof err === 'string' ? err : 'Unknown Error' }); + } + }); } \ No newline at end of file diff --git a/src/api/shared.js b/src/api/shared.js index 5b702be..f310818 100644 --- a/src/api/shared.js +++ b/src/api/shared.js @@ -7,8 +7,6 @@ const Joi = require('joi'); const config = require('../state/config'); const db = require('../db/manager'); -// TODO: Automatically delete expired shared playlists - function lookupShared(playlistId) { const playlistItem = db.getShareCollection().findOne({ 'playlistId': playlistId }); if (!playlistItem) { throw 'Playlist Not Found' } @@ -76,11 +74,14 @@ exports.setupAfterSecurity = async (mstream) => { const jwtOptions = {}; if (req.body.time) { jwtOptions.expiresIn = `${req.body.time}d`; } + const token = jwt.sign(tokenData, config.program.secret, jwtOptions) const sharedItem = { playlistId: playlistId, playlist: req.body.playlist, - token: jwt.sign(tokenData, config.program.secret, jwtOptions) + user: req.user.username, + expires: req.body.time ? jwt.verify(token, config.program.secret).exp : null, + token: token }; db.getShareCollection().insert(sharedItem); diff --git a/src/util/admin.js b/src/util/admin.js index d33dfba..a15bd0d 100644 --- a/src/util/admin.js +++ b/src/util/admin.js @@ -125,6 +125,9 @@ exports.deleteUser = async (username) => { db.getPlaylistCollection().findAndRemove({ 'user': { '$eq': username } }); db.saveUserDB(); + db.getShareCollection().findAndRemove({ 'user': { '$eq': username } }); + db.saveUserDB(); + // TODO: Remove user from scrobbler } diff --git a/webapp/admin/index.js b/webapp/admin/index.js index 2bd7dc6..e38bac5 100644 --- a/webapp/admin/index.js +++ b/webapp/admin/index.js @@ -7,6 +7,9 @@ const ADMINDATA = (() => { // Used for modifying a user module.selectedUser = { value: '' }; + // For lastFM user data on new user form + module.lastFMStorage = { username: '', password: '' }; + module.folders = {}; module.foldersUpdated = { ts: 0 }; module.users = {}; @@ -18,6 +21,60 @@ const ADMINDATA = (() => { module.transcodeParams = {}; module.transcodeParamsUpdated = { ts: 0 }; module.downloadPending = { val: false }; + module.sharedPlaylists = []; + module.sharedPlaylistUpdated = { ts: 0 }; + + module.getSharedPlaylists = async () => { + const res = await API.axios({ + method: 'GET', + url: `${API.url()}/api/v1/admin/db/shared` + }); + console.log(res.data) + + while(module.sharedPlaylists.length !== 0) { + module.sharedPlaylists.pop(); + } + + res.data.forEach(item => { + module.sharedPlaylists.push(item); + }); + + module.sharedPlaylistUpdated.ts = Date.now(); + }; + + module.deleteSharedPlaylist = async (playlistObj) => { + const res = await API.axios({ + method: 'DELETE', + url: `${API.url()}/api/v1/admin/db/shared`, + data: { id: playlistObj.playlistId } + }); + + module.sharedPlaylists.splice(module.sharedPlaylists.indexOf(playlistObj), 1); + }; + + module.deleteUnxpShared = async () => { + const res = await API.axios({ + method: 'DELETE', + url: `${API.url()}/api/v1/admin/db/shared/eternal` + }); + + // Clear playlist array since we no longer know it's state after this api call + while(module.sharedPlaylists.length !== 0) { + module.sharedPlaylists.pop(); + } + }; + + module.deleteExpiredShared = async () => { + const res = await API.axios({ + method: 'DELETE', + url: `${API.url()}/api/v1/admin/db/shared/expired` + }); + + // Clear playlist array since we no longer know it's state after this api call + while(module.sharedPlaylists.length !== 0) { + module.sharedPlaylists.pop(); + } + }; module.getFolders = async () => { const res = await API.axios({ @@ -708,7 +765,10 @@ const dbView = Vue.component('db-view', { return { dbParams: ADMINDATA.dbParams, dbStats: '', - isPullingStats: false + sharedPlaylists: ADMINDATA.sharedPlaylists, + sharedPlaylistsTS: ADMINDATA.sharedPlaylistUpdated, + isPullingStats: false, + isPullingShared: false }; }, template: ` @@ -780,6 +840,47 @@ const dbView = Vue.component('db-view', { +
+
+
+
+ Shared Playlists + Load Playlists +

+
+ +
+
+ [Delete Playlists with no Expiration] +
+ [Delete Expired Playlists] +
+ + + + + + + + + + + + + + + + + +
Playlist IDUserExpiresActions
{{v.playlistId}}{{v.user}}{{v.expires}}[delete]
+
+
+ No Shared Playlists +
+
+
+
+
`, methods: { @@ -791,7 +892,6 @@ const dbView = Vue.component('db-view', { url: `${API.url()}/api/v1/admin/db/scan/stats` }); - console.log(res.data); this.dbStats = res.data } catch (err) { iziToast.error({ @@ -803,6 +903,103 @@ const dbView = Vue.component('db-view', { this.isPullingStats = false; } }, + loadShared: async function() { + try { + this.isPullingShared = true; + await ADMINDATA.getSharedPlaylists(); + } catch (err) { + iziToast.error({ + title: 'Failed to Pull Data', + position: 'topCenter', + timeout: 3500 + }); + } finally { + this.isPullingShared = false; + } + }, + deletePlaylist: async function(playlistObj) { + iziToast.question({ + timeout: 20000, + close: false, + overlayClose: true, + overlay: true, + displayMode: 'once', + id: 'question', + zindex: 99999, + layout: 2, + maxWidth: 600, + title: `Delete playlist ${playlistObj.playlistId}?`, + position: 'center', + buttons: [ + [``, async (instance, toast) => { + instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); + try { + await ADMINDATA.deleteSharedPlaylist(playlistObj); + } catch (err) { + iziToast.error({ + title: 'Failed to Delete Playlist', + position: 'topCenter', + timeout: 3500 + }); + } + }, true], + ['', (instance, toast) => { + instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); + }], + ] + }); + }, + deleteUnxpShared: async function() { + iziToast.question({ + timeout: 20000, + close: false, + overlayClose: true, + overlay: true, + displayMode: 'once', + id: 'question', + zindex: 99999, + layout: 2, + maxWidth: 600, + title: `Delete all playlists without expiration dates?`, + position: 'center', + buttons: [ + [``, async (instance, toast) => { + instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); + try { + this.isPullingShared = true; + await ADMINDATA.deleteUnxpShared(); + await ADMINDATA.getSharedPlaylists(); + } catch (err) { + iziToast.error({ + title: 'Failed to Delete Shared Playlists', + position: 'topCenter', + timeout: 3500 + }); + } finally { + this.isPullingShared = false; + } + }, true], + ['', (instance, toast) => { + instance.hide({ transitionOut: 'fadeOut' }, toast, 'button'); + }], + ] + }); + }, + deleteExpiredShared: async function() { + try { + this.isPullingShared = true; + await ADMINDATA.deleteExpiredShared(); + await ADMINDATA.getSharedPlaylists(); + } catch (err) { + iziToast.error({ + title: 'Failed to Pull Data', + position: 'topCenter', + timeout: 3500 + }); + } finally { + this.isPullingShared = false; + } + }, scanDB: async function() { try { await API.axios({ @@ -1569,7 +1766,7 @@ const usersVpathsView = Vue.component('user-vpaths-view', { template: `