From 162a1b797101a3d6b20e60698069227838579ef3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 25 Sep 2022 17:11:39 -0500 Subject: [PATCH] Add:Purge media progress button & api endpoint for items that no longer exist #921 --- client/pages/config/users/_id/index.vue | 48 +++++++++++-- client/store/globals.js | 10 +-- server/controllers/UserController.js | 89 ++++++++++++++++--------- server/routers/ApiRouter.js | 35 +++++++--- 4 files changed, 129 insertions(+), 53 deletions(-) diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue index e6fe67ed..44fd4b93 100644 --- a/client/pages/config/users/_id/index.vue +++ b/client/pages/config/users/_id/index.vue @@ -46,7 +46,14 @@

Saved Media Progress

- + +
+

User has media progress for {{ mediaProgressWithoutMedia.length }} items that no longer exist.

+
+ Purge Media Progress +
+ +
@@ -54,13 +61,19 @@ - +
Item
-

{{ item.media && item.media.metadata ? item.media.metadata.title : 'Unknown' }}

-

by {{ item.media.metadata.authorName }}

+ +

{{ Math.floor(item.progress * 100) }}%

@@ -98,7 +111,8 @@ export default { data() { return { listeningSessions: [], - listeningStats: {} + listeningStats: {}, + purgingMediaProgress: false } }, computed: { @@ -117,6 +131,12 @@ export default { mediaProgress() { return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate) }, + mediaProgressWithMedia() { + return this.mediaProgress.filter((mp) => mp.media) + }, + mediaProgressWithoutMedia() { + return this.mediaProgress.filter((mp) => !mp.media) + }, totalListeningTime() { return this.listeningStats.totalTime || 0 }, @@ -150,6 +170,24 @@ export default { return [] }) console.log('Loaded user listening data', this.listeningSessions, this.listeningStats) + }, + purgeMediaProgress() { + this.purgingMediaProgress = true + + this.$axios + .$post(`/api/users/${this.user.id}/purge-media-progress`) + .then((updatedUser) => { + console.log('Updated user', updatedUser) + this.$toast.success('Media progress purged') + this.user = updatedUser + }) + .catch((error) => { + console.error('Failed to purge media progress', error) + this.$toast.error('Failed to purge media progress') + }) + .finally(() => { + this.purgingMediaProgress = false + }) } }, mounted() { diff --git a/client/store/globals.js b/client/store/globals.js index 9e837f00..8b1c3d5e 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -40,13 +40,15 @@ export const getters = { // Absolute URL covers (should no longer be used) if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath - var userToken = rootGetters['user/getToken'] - var lastUpdate = libraryItem.updatedAt || Date.now() + const userToken = rootGetters['user/getToken'] + const lastUpdate = libraryItem.updatedAt || Date.now() + const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers if (process.env.NODE_ENV !== 'production') { // Testing - return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` + return `http://localhost:3333/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` } - return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` + + return `/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` }, getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = '/book_placeholder.jpg') => { if (!libraryItemId) return placeholder diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 6e2f4b9b..e7e96926 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -28,10 +28,6 @@ class UserController { } async create(req, res) { - if (!req.user.isAdminOrUp) { - Logger.warn('Non-admin user attempted to create user', req.user) - return res.sendStatus(403) - } var account = req.body var username = account.username @@ -58,15 +54,7 @@ class UserController { } async update(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('[UserController] User other than admin attempting to update user', req.user) - return res.sendStatus(403) - } - - var user = this.db.users.find(u => u.id === req.params.id) - if (!user) { - return res.sendStatus(404) - } + var user = req.reqUser if (user.type === 'root' && !req.user.isRoot) { Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username) @@ -97,9 +85,9 @@ class UserController { Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await this.db.updateEntity('user', user) + this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) } - this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) res.json({ success: true, user: user.toJSONForBrowser() @@ -107,24 +95,15 @@ class UserController { } async delete(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to delete user', req.user) - return res.sendStatus(403) - } if (req.params.id === 'root') { + Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted') return res.sendStatus(500) } if (req.user.id === req.params.id) { - Logger.error('Attempting to delete themselves...') + Logger.error(`[UserController] ${req.user.username} is attempting to delete themselves... why? WHY?`) return res.sendStatus(500) } - var user = this.db.users.find(u => u.id === req.params.id) - if (!user) { - Logger.error('User not found') - return res.json({ - error: 'User not found' - }) - } + var user = req.reqUser // delete user collections var userCollections = this.db.collections.filter(c => c.userId === user.id) @@ -145,10 +124,6 @@ class UserController { // GET: api/users/:id/listening-sessions async getListeningSessions(req, res) { - if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { - return res.sendStatus(403) - } - var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id) const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 @@ -170,11 +145,59 @@ class UserController { // GET: api/users/:id/listening-stats async getListeningStats(req, res) { - if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { - return res.sendStatus(403) - } var listeningStats = await this.getUserListeningStatsHelpers(req.params.id) res.json(listeningStats) } + + // POST: api/users/:id/purge-media-progress + async purgeMediaProgress(req, res) { + const user = req.reqUser + + if (user.type === 'root' && !req.user.isRoot) { + Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username) + return res.sendStatus(403) + } + + var progressPurged = 0 + user.mediaProgress = user.mediaProgress.filter(mp => { + const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) + if (!libraryItem) { + progressPurged++ + return false + } else if (mp.episodeId) { + const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null + if (!episode) { // Episode not found + progressPurged++ + return false + } + } + return true + }) + + if (progressPurged) { + Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`) + await this.db.updateEntity('user', user) + this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) + } + + res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot)) + } + + middleware(req, res, next) { + if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { + return res.sendStatus(403) + } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) { + return res.sendStatus(403) + } + + if (req.params.id) { + req.reqUser = this.db.users.find(u => u.id === req.params.id) + if (!req.reqUser) { + return res.sendStatus(404) + } + } + + next() + } } module.exports = new UserController() \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index d7ad0c07..38d534a1 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -109,14 +109,15 @@ class ApiRouter { // // User Routes // - this.router.post('/users', UserController.create.bind(this)) - this.router.get('/users', UserController.findAll.bind(this)) - this.router.get('/users/:id', UserController.findOne.bind(this)) - this.router.patch('/users/:id', UserController.update.bind(this)) - this.router.delete('/users/:id', UserController.delete.bind(this)) + this.router.post('/users', UserController.middleware.bind(this), UserController.create.bind(this)) + this.router.get('/users', UserController.middleware.bind(this), UserController.findAll.bind(this)) + this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this)) + this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this)) + this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this)) - this.router.get('/users/:id/listening-sessions', UserController.getListeningSessions.bind(this)) - this.router.get('/users/:id/listening-stats', UserController.getListeningStats.bind(this)) + this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this)) + this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this)) + this.router.post('/users/:id/purge-media-progress', UserController.middleware.bind(this), UserController.purgeMediaProgress.bind(this)) // // Collection Routes @@ -274,12 +275,24 @@ class ApiRouter { } json.mediaProgress = json.mediaProgress.map(lip => { - var libraryItem = this.db.libraryItems.find(li => li.id === lip.id) + var libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId) if (!libraryItem) { - Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id) - return null + Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId) + lip.media = null + } else { + if (lip.episodeId) { + const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(lip.episodeId) : null + if (!episode) { + Logger.warn(`[ApiRouter] Episode ${lip.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`) + lip.media = null + } else { + lip.media = libraryItem.media.toJSONExpanded() + lip.episode = episode.toJSON() + } + } else { + lip.media = libraryItem.media.toJSONExpanded() + } } - lip.media = libraryItem.media.toJSONExpanded() return lip }).filter(lip => !!lip)