From afc16358cad63d23dc35d8f846a365ad31235b09 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 15:15:34 -0500 Subject: [PATCH] Update more API endpoints to use new user model --- server/controllers/AuthorController.js | 2 +- server/controllers/CollectionController.js | 2 +- server/controllers/LibraryItemController.js | 29 +++- server/controllers/MeController.js | 1 - server/controllers/MiscController.js | 159 ++++++++++--------- server/controllers/NotificationController.js | 87 +++++++++- server/controllers/PlaylistController.js | 75 ++++++--- server/controllers/PodcastController.js | 134 ++++++++++++---- server/controllers/RSSFeedController.js | 86 ++++++++-- server/controllers/SearchController.js | 60 +++++-- server/controllers/SeriesController.js | 37 +++-- server/controllers/SessionController.js | 125 +++++++++++---- server/controllers/ShareController.js | 43 +++-- server/controllers/ToolsController.js | 47 +++--- server/controllers/UserController.js | 98 +++++++++--- server/managers/AbMergeManager.js | 6 +- server/managers/ApiCacheManager.js | 2 +- server/managers/AudioMetadataManager.js | 20 ++- server/managers/PlaybackSessionManager.js | 116 ++++++++++---- server/managers/RssFeedManager.js | 39 +++-- server/models/User.js | 5 +- server/objects/user/User.js | 83 ---------- server/routers/ApiRouter.js | 4 +- 23 files changed, 856 insertions(+), 404 deletions(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index ec1d648e..74bf3bcc 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -365,7 +365,7 @@ class AuthorController { if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 09525957..6657918c 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -337,7 +337,7 @@ class CollectionController { if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index c73bddf6..dbe47f93 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') const Logger = require('../Logger') @@ -15,6 +16,14 @@ const CacheManager = require('../managers/CacheManager') const CoverManager = require('../managers/CoverManager') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class LibraryItemController { constructor() {} @@ -328,7 +337,14 @@ class LibraryItemController { return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options) } - // POST: api/items/:id/play + /** + * POST: /api/items/:id/play + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ startPlaybackSession(req, res) { if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') { Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) @@ -338,7 +354,14 @@ class LibraryItemController { this.playbackSessionManager.startSessionRequest(req, res, null) } - // POST: api/items/:id/play/:episodeId + /** + * POST: /api/items/:id/play/:episodeId + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ startEpisodePlaybackSession(req, res) { var libraryItem = req.libraryItem if (!libraryItem.media.numTracks) { @@ -830,7 +853,7 @@ class LibraryItemController { } else if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 2699f697..1b883a30 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -12,7 +12,6 @@ const userStats = require('../utils/queries/userStats') * @property {import('../objects/user/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser - * */ class MeController { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 5d560a58..de660e28 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -1,5 +1,6 @@ const Sequelize = require('sequelize') const Path = require('path') +const { Request, Response } = require('express') const fs = require('../libs/fsExtra') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -13,21 +14,27 @@ const { sanitizeFilename } = require('../utils/fileUtils') const TaskManager = require('../managers/TaskManager') const adminStats = require('../utils/queries/adminStats') -// -// This is a controller for routes that don't have a home yet :( -// +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class MiscController { constructor() {} /** * POST: /api/upload * Update library item - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async handleUpload(req, res) { - if (!req.user.canUpload) { - Logger.warn('User attempted to upload without permission', req.user) + if (!req.userNew.canUpload) { + Logger.warn(`User "${req.userNew.username}" attempted to upload without permission`) return res.sendStatus(403) } if (!req.files) { @@ -83,8 +90,9 @@ class MiscController { /** * GET: /api/tasks * Get tasks for task manager - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ getTasks(req, res) { const includeArray = (req.query.include || '').split(',') @@ -106,12 +114,12 @@ class MiscController { * PATCH: /api/settings * Update server settings * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateServerSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to update server settings', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`User "${req.userNew.username}" other than admin attempting to update server settings`) return res.sendStatus(403) } const settingsUpdate = req.body @@ -137,12 +145,12 @@ class MiscController { /** * PATCH: /api/sorting-prefixes * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateSortingPrefixes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to update server sorting prefixes', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`User "${req.userNew.username}" other than admin attempting to update server sorting prefixes`) return res.sendStatus(403) } let sortingPrefixes = req.body.sortingPrefixes @@ -237,14 +245,10 @@ class MiscController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async authorize(req, res) { - if (!req.user) { - Logger.error('Invalid user in authorize') - return res.sendStatus(401) - } const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew) res.json(userResponse) } @@ -252,13 +256,14 @@ class MiscController { /** * GET: /api/tags * Get all tags - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getAllTags(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to getAllTags`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllTags`) + return res.sendStatus(403) } const tags = [] @@ -295,13 +300,14 @@ class MiscController { * POST: /api/tags/rename * Rename tag * Req.body { tag, newTag } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async renameTag(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to renameTag`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameTag`) + return res.sendStatus(403) } const tag = req.body.tag @@ -349,13 +355,14 @@ class MiscController { * DELETE: /api/tags/:tag * Remove a tag * :tag param is base64 encoded - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async deleteTag(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to deleteTag`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteTag`) + return res.sendStatus(403) } const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() @@ -388,13 +395,14 @@ class MiscController { /** * GET: /api/genres * Get all genres - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getAllGenres(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllGenres`) + return res.sendStatus(403) } const genres = [] const books = await Database.bookModel.findAll({ @@ -430,13 +438,14 @@ class MiscController { * POST: /api/genres/rename * Rename genres * Req.body { genre, newGenre } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async renameGenre(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to renameGenre`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameGenre`) + return res.sendStatus(403) } const genre = req.body.genre @@ -484,13 +493,14 @@ class MiscController { * DELETE: /api/genres/:genre * Remove a genre * :genre param is base64 encoded - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async deleteGenre(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteGenre`) + return res.sendStatus(403) } const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() @@ -526,15 +536,16 @@ class MiscController { * Req.body { libraryId, path, type, [oldPath] } * type = add, unlink, rename * oldPath = required only for rename + * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ updateWatchedPath(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to updateWatchedPath`) + return res.sendStatus(403) } const libraryId = req.body.libraryId @@ -586,12 +597,12 @@ class MiscController { /** * GET: api/auth-settings (admin only) * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ getAuthSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get auth settings`) return res.sendStatus(403) } return res.json(Database.serverSettings.authenticationSettings) @@ -601,12 +612,12 @@ class MiscController { * PATCH: api/auth-settings * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateAuthSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to update auth settings`) return res.sendStatus(403) } @@ -706,12 +717,12 @@ class MiscController { /** * GET: /api/stats/year/:year * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAdminStatsForYear(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin stats for year`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get admin stats for year`) return res.sendStatus(403) } const year = Number(req.params.year) @@ -727,12 +738,12 @@ class MiscController { * GET: /api/logger-data * admin or up * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getLoggerData(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get logger data`) return res.sendStatus(403) } diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js index 8b94a9bb..fb1c0fe1 100644 --- a/server/controllers/NotificationController.js +++ b/server/controllers/NotificationController.js @@ -1,10 +1,27 @@ -const Logger = require('../Logger') +const { Request, Response, NextFunction } = require('express') const Database = require('../Database') const { version } = require('../../package.json') -class NotificationController { - constructor() { } +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ +class NotificationController { + constructor() {} + + /** + * GET: /api/notifications + * Get notifications, settings and data + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ get(req, res) { res.json({ data: this.notificationManager.getData(), @@ -12,6 +29,12 @@ class NotificationController { }) } + /** + * PATCH: /api/notifications + * + * @param {RequestWithUser} req + * @param {Response} res + */ async update(req, res) { const updated = Database.notificationSettings.update(req.body) if (updated) { @@ -20,15 +43,38 @@ class NotificationController { res.sendStatus(200) } + /** + * GET: /api/notificationdata + * @deprecated Use /api/notifications + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ getData(req, res) { res.json(this.notificationManager.getData()) } + /** + * GET: /api/notifications/test + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async fireTestEvent(req, res) { await this.notificationManager.triggerNotification('onTest', { version: `v${version}` }, req.query.fail === '1') res.sendStatus(200) } + /** + * POST: /api/notifications + * + * @param {RequestWithUser} req + * @param {Response} res + */ async createNotification(req, res) { const success = Database.notificationSettings.createNotification(req.body) @@ -38,6 +84,12 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * DELETE: /api/notifications/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async deleteNotification(req, res) { if (Database.notificationSettings.removeNotification(req.notification.id)) { await Database.updateSetting(Database.notificationSettings) @@ -45,6 +97,12 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * PATCH: /api/notifications/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateNotification(req, res) { const success = Database.notificationSettings.updateNotification(req.body) if (success) { @@ -53,17 +111,32 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * GET: /api/notifications/:id/test + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async sendNotificationTest(req, res) { - if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured') + if (!Database.notificationSettings.isUseable) return res.status(400).send('Apprise is not configured') const success = await this.notificationManager.sendTestNotification(req.notification) if (success) res.sendStatus(200) else res.sendStatus(500) } + /** + * Requires admin or up + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + return res.sendStatus(403) } if (req.params.id) { @@ -77,4 +150,4 @@ class NotificationController { next() } } -module.exports = new NotificationController() \ No newline at end of file +module.exports = new NotificationController() diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 94c769b1..9428bca0 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -1,21 +1,31 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const Playlist = require('../objects/Playlist') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class PlaylistController { constructor() {} /** * POST: /api/playlists * Create playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { const oldPlaylist = new Playlist() - req.body.userId = req.user.id + req.body.userId = req.userNew.id const success = oldPlaylist.setData(req.body) if (!success) { return res.status(400).send('Invalid playlist request data') @@ -58,13 +68,14 @@ class PlaylistController { /** * GET: /api/playlists * Get all playlists for user - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.findAll({ where: { - userId: req.user.id + userId: req.userNew.id } }) const playlists = [] @@ -79,8 +90,9 @@ class PlaylistController { /** * GET: /api/playlists/:id - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -90,8 +102,9 @@ class PlaylistController { /** * PATCH: /api/playlists/:id * Update playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async update(req, res) { const updatedPlaylist = req.playlist.set(req.body) @@ -156,8 +169,9 @@ class PlaylistController { /** * DELETE: /api/playlists/:id * Remove playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -169,8 +183,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/item * Add item to playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addItem(req, res) { const oldPlaylist = await Database.playlistModel.getById(req.playlist.id) @@ -213,8 +228,9 @@ class PlaylistController { /** * DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId? * Remove item from playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeItem(req, res) { const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId) @@ -266,8 +282,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/add * Batch add playlist items - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addBatch(req, res) { if (!req.body.items?.length) { @@ -330,8 +347,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/remove * Batch remove playlist items - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeBatch(req, res) { if (!req.body.items?.length) { @@ -387,8 +405,9 @@ class PlaylistController { /** * POST: /api/playlists/collection/:collectionId * Create a playlist from a collection - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async createFromCollection(req, res) { const collection = await Database.collectionModel.findByPk(req.params.collectionId) @@ -409,7 +428,7 @@ class PlaylistController { const oldPlaylist = new Playlist() oldPlaylist.setData({ - userId: req.user.id, + userId: req.userNew.id, libraryId: collection.libraryId, name: collection.name, description: collection.description || null @@ -436,14 +455,20 @@ class PlaylistController { res.json(jsonExpanded) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { if (req.params.id) { const playlist = await Database.playlistModel.findByPk(req.params.id) if (!playlist) { return res.status(404).send('Playlist not found') } - if (playlist.userId !== req.user.id) { - Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`) + if (playlist.userId !== req.userNew.id) { + Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.userNew.id} that is not the owner`) return res.sendStatus(403) } req.playlist = playlist diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index b20547e3..a7346546 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -13,6 +14,14 @@ const CoverManager = require('../managers/CoverManager') const LibraryItem = require('../objects/LibraryItem') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class PodcastController { /** * POST /api/podcasts @@ -20,12 +29,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to create podcast`) return res.sendStatus(403) } const payload = req.body @@ -121,12 +130,12 @@ class PodcastController { * @typedef getPodcastFeedReqBody * @property {string} rssFeed * - * @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req - * @param {import('express').Response} res + * @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObjects} req + * @param {Response} res */ async getPodcastFeed(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get podcast feed`) return res.sendStatus(403) } @@ -147,12 +156,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getFeedsFromOPMLText(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get feeds from opml`) return res.sendStatus(403) } @@ -170,12 +179,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async bulkCreatePodcastsFromOpmlFeedUrls(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to bulk create podcasts`) return res.sendStatus(403) } @@ -200,9 +209,17 @@ class PodcastController { res.sendStatus(200) } + /** + * GET: /api/podcasts/:id/checknew + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ async checkNewEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to check/download episodes`) return res.sendStatus(403) } @@ -220,15 +237,31 @@ class PodcastController { }) } + /** + * GET: /api/podcasts/:id/clear-queue + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ clearEpisodeDownloadQueue(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempting to clear download queue`) return res.sendStatus(403) } this.podcastManager.clearDownloadQueue(req.params.id) res.sendStatus(200) } + /** + * GET: /api/podcasts/:id/downloads + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ getEpisodeDownloads(req, res) { var libraryItem = req.libraryItem @@ -255,9 +288,17 @@ class PodcastController { }) } + /** + * POST: /api/podcasts/:id/download-episodes + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async downloadEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) return res.sendStatus(403) } const libraryItem = req.libraryItem @@ -270,10 +311,17 @@ class PodcastController { res.sendStatus(200) } - // POST: api/podcasts/:id/match-episodes + /** + * POST: /api/podcasts/:id/match-episodes + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async quickMatchEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) return res.sendStatus(403) } @@ -289,6 +337,12 @@ class PodcastController { }) } + /** + * PATCH: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateEpisode(req, res) { const libraryItem = req.libraryItem @@ -305,7 +359,12 @@ class PodcastController { res.json(libraryItem.toJSONExpanded()) } - // GET: api/podcasts/:id/episode/:episodeId + /** + * GET: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getEpisode(req, res) { const episodeId = req.params.episodeId const libraryItem = req.libraryItem @@ -319,7 +378,12 @@ class PodcastController { res.json(episode) } - // DELETE: api/podcasts/:id/episode/:episodeId + /** + * DELETE: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeEpisode(req, res) { const episodeId = req.params.episodeId const libraryItem = req.libraryItem @@ -390,6 +454,12 @@ class PodcastController { res.json(libraryItem.toJSON()) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const item = await Database.libraryItemModel.getOldById(req.params.id) if (!item?.media) return res.sendStatus(404) @@ -399,15 +469,15 @@ class PodcastController { } // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { + if (!req.userNew.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[PodcastController] User attempted to delete without permission`, req.user.username) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[PodcastController] User attempted to update without permission', req.user.username) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 9b7acf70..ac79764f 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -1,19 +1,43 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') -class RSSFeedController { - constructor() { } +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ +class RSSFeedController { + constructor() {} + + /** + * GET: /api/feeds + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getAll(req, res) { const feeds = await this.rssFeedManager.getFeeds() res.json({ - feeds: feeds.map(f => f.toJSON()), - minified: feeds.map(f => f.toJSONMinified()) + feeds: feeds.map((f) => f.toJSON()), + minified: feeds.map((f) => f.toJSONMinified()) }) } - // POST: api/feeds/item/:itemId/open + /** + * POST: /api/feeds/item/:itemId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForItem(req, res) { const options = req.body || {} @@ -21,8 +45,8 @@ class RSSFeedController { if (!item) return res.sendStatus(404) // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { - Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) + if (!req.userNew.checkCanAccessLibraryItem(item)) { + Logger.error(`[RSSFeedController] User "${req.userNew.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) return res.sendStatus(403) } @@ -44,13 +68,20 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - const feed = await this.rssFeedManager.openFeedForItem(req.user, item, req.body) + const feed = await this.rssFeedManager.openFeedForItem(req.userNew.id, item, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/collection/:collectionId/open + /** + * POST: /api/feeds/collection/:collectionId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForCollection(req, res) { const options = req.body || {} @@ -70,7 +101,7 @@ class RSSFeedController { } const collectionExpanded = await collection.getOldJsonExpanded() - const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length) + const collectionItemsWithTracks = collectionExpanded.books.filter((li) => li.media.tracks.length) // Check collection has audio tracks if (!collectionItemsWithTracks.length) { @@ -78,13 +109,20 @@ class RSSFeedController { return res.status(400).send('Collection has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForCollection(req.user, collectionExpanded, req.body) + const feed = await this.rssFeedManager.openFeedForCollection(req.userNew.id, collectionExpanded, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/series/:seriesId/open + /** + * POST: /api/feeds/series/:seriesId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForSeries(req, res) { const options = req.body || {} @@ -106,7 +144,7 @@ class RSSFeedController { const seriesJson = series.toJSON() // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) // Check series has audio tracks if (!seriesJson.books.length) { @@ -114,20 +152,34 @@ class RSSFeedController { return res.status(400).send('Series has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForSeries(req.user, seriesJson, req.body) + const feed = await this.rssFeedManager.openFeedForSeries(req.userNew.id, seriesJson, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/:id/close + /** + * POST: /api/feeds/:id/close + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ closeRSSFeed(req, res) { this.rssFeedManager.closeRssFeed(req, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ middleware(req, res, next) { - if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds - Logger.error(`[RSSFeedController] Non-admin user attempted to make a request to an RSS feed route`, req.user.username) + if (!req.userNew.isAdminOrUp) { + // Only admins can manage rss feeds + Logger.error(`[RSSFeedController] Non-admin user "${req.userNew.username}" attempted to make a request to an RSS feed route`) return res.sendStatus(403) } diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index b0aebb31..7317faf4 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const Logger = require('../Logger') const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') @@ -6,25 +7,51 @@ const MusicFinder = require('../finders/MusicFinder') const Database = require('../Database') const { isValidASIN } = require('../utils') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SearchController { constructor() {} + /** + * GET: /api/search/books + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findBooks(req, res) { const id = req.query.id const libraryItem = await Database.libraryItemModel.getOldById(id) const provider = req.query.provider || 'google' const title = req.query.title || '' const author = req.query.author || '' + + if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') { + Logger.error(`[SearchController] findBooks: Invalid request query params`) + return res.status(400).send('Invalid request query params') + } + const results = await BookFinder.search(libraryItem, provider, title, author) res.json(results) } + /** + * GET: /api/search/covers + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findCovers(req, res) { const query = req.query const podcast = query.podcast == 1 - if (!query.title) { - Logger.error(`[SearchController] findCovers: No title sent in query`) + if (!query.title || typeof query.title !== 'string') { + Logger.error(`[SearchController] findCovers: Invalid title sent in query`) return res.sendStatus(400) } @@ -37,10 +64,11 @@ class SearchController { } /** + * GET: /api/search/podcasts * Find podcast RSS feeds given a term * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async findPodcasts(req, res) { const term = req.query.term @@ -56,12 +84,29 @@ class SearchController { res.json(results) } + /** + * GET: /api/search/authors + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findAuthor(req, res) { const query = req.query.q + if (!query || typeof query !== 'string') { + Logger.error(`[SearchController] findAuthor: Invalid query param`) + return res.status(400).send('Invalid query param') + } + const author = await AuthorFinder.findAuthorByName(query) res.json(author) } + /** + * GET: /api/search/chapters + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findChapters(req, res) { const asin = req.query.asin if (!isValidASIN(asin.toUpperCase())) { @@ -74,12 +119,5 @@ class SearchController { } res.json(chapterData) } - - async findMusicTrack(req, res) { - const tracks = await MusicFinder.searchTrack(req.query || {}) - res.json({ - tracks - }) - } } module.exports = new SearchController() diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index b3adec4b..a08af1e3 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -1,8 +1,17 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SeriesController { constructor() {} @@ -13,8 +22,8 @@ class SeriesController { * TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead * Series are not library specific so we need to know what the library id is * - * @param {*} req - * @param {*} res + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const include = (req.query.include || '') @@ -28,8 +37,7 @@ class SeriesController { if (include.includes('progress')) { const libraryItemsInSeries = req.libraryItemsInSeries const libraryItemsFinished = libraryItemsInSeries.filter((li) => { - const mediaProgress = req.user.getMediaProgress(li.id) - return mediaProgress?.isFinished + return req.userNew.getMediaProgress(li.media.id)?.isFinished }) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map((li) => li.id), @@ -46,6 +54,11 @@ class SeriesController { res.json(seriesJson) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + */ async update(req, res) { const hasUpdated = req.series.update(req.body) if (hasUpdated) { @@ -55,6 +68,12 @@ class SeriesController { res.json(req.series.toJSON()) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const series = await Database.seriesModel.getOldById(req.params.id) if (!series) return res.sendStatus(404) @@ -64,15 +83,15 @@ class SeriesController { */ const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.userNew) if (!libraryItems.length) { - Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user) + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to access series "${series.id}" with no accessible books`) return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[SeriesController] User attempted to update without permission', req.user) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 9dd3666d..882528c9 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -1,26 +1,32 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') const { toNumber, isUUID } = require('../utils/index') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SessionController { constructor() {} - async findOne(req, res) { - return res.json(req.playbackSession) - } - /** * GET: /api/sessions + * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAllWithUserData(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] getAllWithUserData: Non-admin user "${req.userNew.username}" requested all session data`) return res.sendStatus(404) } // Validate "user" query @@ -105,9 +111,17 @@ class SessionController { res.json(payload) } + /** + * GET: /api/sessions/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOpenSessions(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] getOpenSessions: Non-admin user "${req.userNew.username}" requested open session data`) return res.sendStatus(404) } @@ -127,25 +141,54 @@ class SessionController { }) } + /** + * GET: /api/session/:id + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOpenSession(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) res.json(sessionForClient) } - // POST: api/session/:id/sync + /** + * POST: /api/session/:id/sync + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.userNew, req.playbackSession, req.body, res) } - // POST: api/session/:id/close + /** + * POST: /api/session/:id/close + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.userNew, req.playbackSession, syncData, res) } - // DELETE: api/session/:id + /** + * DELETE: /api/session/:id + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async delete(req, res) { // if session is open then remove it const openSession = this.playbackSessionManager.getSession(req.playbackSession.id) @@ -164,12 +207,12 @@ class SessionController { * @typedef batchDeleteReqBody * @property {string[]} sessions * - * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req - * @param {import('express').Response} res + * @param {Request<{}, {}, batchDeleteReqBody, {}> & RequestUserObjects} req + * @param {Response} res */ async batchDelete(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] Non-admin user "${req.userNew.username}" attempted to batch delete sessions`) return res.sendStatus(403) } // Validate session ids @@ -192,7 +235,7 @@ class SessionController { id: req.body.sessions } }) - Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`) + Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.userNew.username}"`) res.sendStatus(200) } catch (error) { Logger.error(`[SessionController] Failed to remove playback sessions`, error) @@ -200,22 +243,42 @@ class SessionController { } } - // POST: api/session/local + /** + * POST: /api/session/local + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ syncLocal(req, res) { this.playbackSessionManager.syncLocalSessionRequest(req, res) } - // POST: api/session/local-all + /** + * POST: /api/session/local-all + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ syncLocalSessions(req, res) { this.playbackSessionManager.syncLocalSessionsRequest(req, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ openSessionMiddleware(req, res, next) { var playbackSession = this.playbackSessionManager.getSession(req.params.id) if (!playbackSession) return res.sendStatus(404) - if (playbackSession.userId !== req.user.id) { - Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`) + if (playbackSession.userId !== req.userNew.id) { + Logger.error(`[SessionController] User "${req.userNew.username}" attempting to access session belonging to another user "${req.params.id}"`) return res.sendStatus(404) } @@ -223,6 +286,12 @@ class SessionController { next() } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const playbackSession = await Database.getPlaybackSession(req.params.id) if (!playbackSession) { @@ -230,11 +299,11 @@ class SessionController { return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[SessionController] User attempted to delete without permission`, req.user) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[SessionController] User attempted to update without permission', req.user.username) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 0dbec374..08225b60 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const uuid = require('uuid') const Path = require('path') const { Op } = require('sequelize') @@ -10,6 +11,14 @@ const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUti const PlaybackSession = require('../objects/PlaybackSession') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class ShareController { constructor() {} @@ -20,8 +29,8 @@ class ShareController { * * @this {import('../routers/PublicRouter')} * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareBySlug(req, res) { const { slug } = req.params @@ -122,8 +131,8 @@ class ShareController { * GET: /api/share/:slug/cover * Get media item share cover image * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareCoverImage(req, res) { if (!req.cookies.share_session_id) { @@ -162,8 +171,8 @@ class ShareController { * GET: /api/share/:slug/track/:index * Get media item share audio track * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareAudioTrack(req, res) { if (!req.cookies.share_session_id) { @@ -208,8 +217,8 @@ class ShareController { * PATCH: /api/share/:slug/progress * Update media item share progress * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async updateMediaItemShareProgress(req, res) { if (!req.cookies.share_session_id) { @@ -242,12 +251,12 @@ class ShareController { * POST: /api/share/mediaitem * Create a new media item share * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async createMediaItemShare(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to create item share`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to create item share`) return res.sendStatus(403) } @@ -290,7 +299,7 @@ class ShareController { expiresAt: expiresAt || null, mediaItemId, mediaItemType, - userId: req.user.id + userId: req.userNew.id }) ShareManager.openMediaItemShare(mediaItemShare) @@ -306,12 +315,12 @@ class ShareController { * DELETE: /api/share/mediaitem/:id * Delete media item share * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async deleteMediaItemShare(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to delete item share`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to delete item share`) return res.sendStatus(403) } diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 102dd030..54c55948 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -1,6 +1,15 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class ToolsController { constructor() {} @@ -10,8 +19,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async encodeM4b(req, res) { if (req.libraryItem.isMissing || req.libraryItem.isInvalid) { @@ -30,7 +39,7 @@ class ToolsController { } const options = req.query || {} - this.abMergeManager.startAudiobookMerge(req.user, req.libraryItem, options) + this.abMergeManager.startAudiobookMerge(req.userNew.id, req.libraryItem, options) res.sendStatus(200) } @@ -41,8 +50,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async cancelM4bEncode(req, res) { const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id) @@ -59,8 +68,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async embedAudioFileMetadata(req, res) { if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) { @@ -77,7 +86,7 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, options) + this.audioMetadataManager.updateMetadataForItem(req.userNew.id, req.libraryItem, options) res.sendStatus(200) } @@ -87,8 +96,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async batchEmbedMetadata(req, res) { const libraryItemIds = req.body.libraryItemIds || [] @@ -105,8 +114,8 @@ class ToolsController { } // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(libraryItem)) { - Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user`, req.user) + if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user "${req.userNew.username}"`) return res.sendStatus(403) } @@ -127,19 +136,19 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.handleBatchEmbed(req.user, libraryItems, options) + this.audioMetadataManager.handleBatchEmbed(req.userNew.id, libraryItems, options) res.sendStatus(200) } /** * - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next */ async middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-root user "${req.userNew.username}" attempted to access tools route`) return res.sendStatus(403) } @@ -148,7 +157,7 @@ class ToolsController { if (!item?.media) return res.sendStatus(404) // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { + if (!req.userNew.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index fdb6d194..2916de1b 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const uuidv4 = require('uuid').v4 const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -8,12 +9,18 @@ const User = require('../objects/user/User') const { toNumber } = require('../utils/index') /** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + * * @typedef UserControllerRequestProps + * @property {import('../models/User')} userNew * @property {import('../objects/user/User')} user - User that made the request * @property {import('../objects/user/User')} [reqUser] - User for req param id * - * @typedef {import('express').Request & UserControllerRequestProps} UserControllerRequest - * @typedef {import('express').Response} UserControllerResponse + * @typedef {Request & UserControllerRequestProps} UserControllerRequest */ class UserController { @@ -22,11 +29,11 @@ class UserController { /** * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async findAll(req, res) { - if (!req.user.isAdminOrUp) return res.sendStatus(403) - const hideRootToken = !req.user.isRoot + if (!req.userNew.isAdminOrUp) return res.sendStatus(403) + const hideRootToken = !req.userNew.isRoot const includes = (req.query.include || '').split(',').map((i) => i.trim()) @@ -52,11 +59,11 @@ class UserController { * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt` * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async findOne(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to get user', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`Non-admin user "${req.userNew.username}" attempted to get user`) return res.sendStatus(403) } @@ -95,13 +102,22 @@ class UserController { return oldMediaProgress }) - const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot) + const userJson = req.reqUser.toJSONForBrowser(!req.userNew.isRoot) userJson.mediaProgress = oldMediaProgresses res.json(userJson) } + /** + * POST: /api/users + * Create a new user + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async create(req, res) { const account = req.body const username = account.username @@ -134,13 +150,13 @@ class UserController { * Update user * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async update(req, res) { const user = req.reqUser - if (user.type === 'root' && !req.user.isRoot) { - Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username) + if (user.type === 'root' && !req.userNew.isRoot) { + Logger.error(`[UserController] Admin user "${req.userNew.username}" attempted to update root user`) return res.sendStatus(403) } @@ -168,7 +184,7 @@ class UserController { Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await Database.updateUser(user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', user.toJSONForBrowser()) } res.json({ @@ -177,14 +193,21 @@ class UserController { }) } + /** + * DELETE: /api/users/:id + * Delete a user + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async delete(req, res) { if (req.params.id === 'root') { Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted') - return res.sendStatus(500) + return res.sendStatus(400) } - if (req.user.id === req.params.id) { - Logger.error(`[UserController] ${req.user.username} is attempting to delete themselves... why? WHY?`) - return res.sendStatus(500) + if (req.userNew.id === req.params.id) { + Logger.error(`[UserController] User ${req.userNew.username} is attempting to delete self`) + return res.sendStatus(400) } const user = req.reqUser @@ -212,20 +235,25 @@ class UserController { * PATCH: /api/users/:id/openid-unlink * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async unlinkFromOpenID(req, res) { Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) req.reqUser.authOpenIDSub = null if (await Database.userModel.updateFromOld(req.reqUser)) { - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.reqUser.toJSONForBrowser()) res.sendStatus(200) } else { res.sendStatus(500) } } - // GET: api/users/:id/listening-sessions + /** + * GET: /api/users/:id/listening-sessions + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async getListeningSessions(req, res) { var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id) @@ -246,15 +274,29 @@ class UserController { res.json(payload) } - // GET: api/users/:id/listening-stats + /** + * GET: /api/users/:id/listening-stats + * + * @this {import('../routers/ApiRouter')} + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async getListeningStats(req, res) { var listeningStats = await this.getUserListeningStatsHelpers(req.params.id) res.json(listeningStats) } - // POST: api/users/online (admin) + /** + * GET: /api/users/online + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOnlineUsers(req, res) { - if (!req.user.isAdminOrUp) { + if (!req.userNew.isAdminOrUp) { return res.sendStatus(403) } @@ -264,10 +306,16 @@ class UserController { }) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { - if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { + if (!req.userNew.isAdminOrUp && req.userNew.id !== req.params.id) { return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) { + } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.userNew.isAdminOrUp) { return res.sendStatus(403) } diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index e0780cc4..77702d79 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -46,11 +46,11 @@ class AbMergeManager { /** * - * @param {import('../objects/user/User')} user + * @param {string} userId * @param {import('../objects/LibraryItem')} libraryItem * @param {AbMergeEncodeOptions} [options={}] */ - async startAudiobookMerge(user, libraryItem, options = {}) { + async startAudiobookMerge(userId, libraryItem, options = {}) { const task = new Task() const audiobookDirname = Path.basename(libraryItem.path) @@ -61,7 +61,7 @@ class AbMergeManager { const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, - userId: user.id, + userId, originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path), inos: libraryItem.media.includedAudioFiles.map((f) => f.ino), tempFilepath, diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 35009447..3b425cb1 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -42,7 +42,7 @@ class ApiCacheManager { Logger.debug(`[ApiCacheManager] Skipping cache for random sort`) return next() } - const key = { user: req.user.username, url: req.url } + const key = { user: req.userNew.username, url: req.url } const stringifiedKey = JSON.stringify(key) Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) const cached = this.cache.get(stringifiedKey) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 39c03ae7..f970d5a8 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -32,13 +32,25 @@ class AudioMetadataMangaer { return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length) } - handleBatchEmbed(user, libraryItems, options = {}) { + /** + * + * @param {string} userId + * @param {*} libraryItems + * @param {*} options + */ + handleBatchEmbed(userId, libraryItems, options = {}) { libraryItems.forEach((li) => { - this.updateMetadataForItem(user, li, options) + this.updateMetadataForItem(userId, li, options) }) } - async updateMetadataForItem(user, libraryItem, options = {}) { + /** + * + * @param {string} userId + * @param {*} libraryItem + * @param {*} options + */ + async updateMetadataForItem(userId, libraryItem, options = {}) { const forceEmbedChapters = !!options.forceEmbedChapters const backupFiles = !!options.backup @@ -58,7 +70,7 @@ class AudioMetadataMangaer { const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, - userId: user.id, + userId, audioFiles: audioFiles.map((af) => ({ index: af.index, ino: af.ino, diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 73a04324..81372ef1 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -39,7 +39,7 @@ class PlaybackSessionManager { /** * - * @param {import('express').Request} req + * @param {import('../controllers/SessionController').RequestWithUser} req * @param {Object} [clientDeviceInfo] * @returns {Promise} */ @@ -48,7 +48,7 @@ class PlaybackSessionManager { const ip = requestIp.getClientIp(req) const deviceInfo = new DeviceInfo() - deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id) + deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.userNew?.id) if (clientDeviceInfo?.deviceId) { const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId) @@ -67,18 +67,25 @@ class PlaybackSessionManager { /** * - * @param {import('express').Request} req + * @param {import('../controllers/SessionController').RequestWithUser} req * @param {import('express').Response} res * @param {string} [episodeId] */ async startSessionRequest(req, res, episodeId) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) - const { user, libraryItem, body: options } = req - const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) + const { libraryItem, body: options } = req + const session = await this.startSession(req.userNew, deviceInfo, libraryItem, episodeId, options) res.json(session.toJSONForClient(libraryItem)) } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} payload + * @param {import('express').Response} res + */ async syncSessionRequest(user, session, payload, res) { if (await this.syncSession(user, session, payload)) { res.sendStatus(200) @@ -89,7 +96,7 @@ class PlaybackSessionManager { async syncLocalSessionsRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) - const user = req.user + const user = req.userNew const sessions = req.body.sessions || [] const syncResults = [] @@ -104,6 +111,13 @@ class PlaybackSessionManager { }) } + /** + * + * @param {import('../models/User')} user + * @param {*} sessionJson + * @param {*} deviceInfo + * @returns + */ async syncLocalSession(user, sessionJson, deviceInfo) { const libraryItem = await Database.libraryItemModel.getOldById(sessionJson.libraryItemId) const episode = sessionJson.episodeId && libraryItem && libraryItem.isPodcast ? libraryItem.media.getEpisode(sessionJson.episodeId) : null @@ -174,41 +188,58 @@ class PlaybackSessionManager { progressSynced: false } - const userProgressForItem = user.getMediaProgress(session.libraryItemId, session.episodeId) + const mediaItemId = session.episodeId || libraryItem.media.id + let userProgressForItem = user.getMediaProgress(mediaItemId) if (userProgressForItem) { - if (userProgressForItem.lastUpdate > session.updatedAt) { + if (userProgressForItem.updatedAt.valueOf() > session.updatedAt) { Logger.debug(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently`) } else { Logger.debug(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`) - result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, + ...session.mediaProgressObject + }) + result.progressSynced = !!updateResponse.mediaProgress + if (result.progressSynced) { + userProgressForItem = updateResponse.mediaProgress + } } } else { Logger.debug(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`) - result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, + ...session.mediaProgressObject + }) + result.progressSynced = !!updateResponse.mediaProgress + if (result.progressSynced) { + userProgressForItem = updateResponse.mediaProgress + } } // Update user and emit socket event if (result.progressSynced) { - const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) - if (itemProgress) { - await Database.upsertMediaProgress(itemProgress) - SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { - id: itemProgress.id, - sessionId: session.id, - deviceDescription: session.deviceDescription, - data: itemProgress.toJSON() - }) - } + SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { + id: userProgressForItem.id, + sessionId: session.id, + deviceDescription: session.deviceDescription, + data: userProgressForItem.getOldMediaProgress() + }) } return result } + /** + * + * @param {import('../controllers/SessionController').RequestWithUser} req + * @param {*} res + */ async syncLocalSessionRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) - const user = req.user const sessionJson = req.body - const result = await this.syncLocalSession(user, sessionJson, deviceInfo) + const result = await this.syncLocalSession(req.userNew, sessionJson, deviceInfo) if (result.error) { res.status(500).send(result.error) } else { @@ -216,6 +247,13 @@ class PlaybackSessionManager { } } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @param {import('express').Response} res + */ async closeSessionRequest(user, session, syncData, res) { await this.closeSession(user, session, syncData) res.sendStatus(200) @@ -223,7 +261,7 @@ class PlaybackSessionManager { /** * - * @param {import('../objects/user/User')} user + * @param {import('../models/User')} user * @param {DeviceInfo} deviceInfo * @param {import('../objects/LibraryItem')} libraryItem * @param {string|null} episodeId @@ -241,7 +279,8 @@ class PlaybackSessionManager { const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId)) const mediaPlayer = options.mediaPlayer || 'unknown' - const userProgress = libraryItem.isMusic ? null : user.getMediaProgress(libraryItem.id, episodeId) + const mediaItemId = episodeId || libraryItem.media.id + const userProgress = user.getMediaProgress(mediaItemId) let userStartTime = 0 if (userProgress) { if (userProgress.isFinished) { @@ -292,6 +331,13 @@ class PlaybackSessionManager { return newPlaybackSession } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @returns + */ async syncSession(user, session, syncData) { const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId) if (!libraryItem) { @@ -303,20 +349,19 @@ class PlaybackSessionManager { session.addListeningTime(syncData.timeListened) Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" (Device: ${session.deviceDescription}) | Total Time Listened: ${session.timeListening}`) - const itemProgressUpdate = { + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, duration: syncData.duration, currentTime: syncData.currentTime, progress: session.progress - } - const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) - if (wasUpdated) { - const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) - if (itemProgress) await Database.upsertMediaProgress(itemProgress) + }) + if (updateResponse.mediaProgress) { SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { - id: itemProgress.id, + id: updateResponse.mediaProgress.id, sessionId: session.id, deviceDescription: session.deviceDescription, - data: itemProgress.toJSON() + data: updateResponse.mediaProgress.getOldMediaProgress() }) } this.saveSession(session) @@ -325,6 +370,13 @@ class PlaybackSessionManager { } } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @returns + */ async closeSession(user, session, syncData = null) { if (syncData) { await this.syncSession(user, session, syncData) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 3149689d..35ce4e1f 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -9,7 +9,7 @@ const Feed = require('../objects/Feed') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RssFeedManager { - constructor() { } + constructor() {} async validateFeedEntity(feedObj) { if (feedObj.entityType === 'collection') { @@ -44,7 +44,7 @@ class RssFeedManager { const feeds = await Database.feedModel.getOldFeeds() for (const feed of feeds) { // Remove invalid feeds - if (!await this.validateFeedEntity(feed)) { + if (!(await this.validateFeedEntity(feed))) { await Database.removeFeed(feed.id) } } @@ -138,7 +138,7 @@ class RssFeedManager { const seriesJson = series.toJSON() // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) // Find most recently updated item in series let mostRecentlyUpdatedAt = seriesJson.updatedAt @@ -202,7 +202,14 @@ class RssFeedManager { readStream.pipe(res) } - async openFeedForItem(user, libraryItem, options) { + /** + * + * @param {string} userId + * @param {*} libraryItem + * @param {*} options + * @returns + */ + async openFeedForItem(userId, libraryItem, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -210,7 +217,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromItem(user.id, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) @@ -218,7 +225,14 @@ class RssFeedManager { return feed } - async openFeedForCollection(user, collectionExpanded, options) { + /** + * + * @param {string} userId + * @param {*} collectionExpanded + * @param {*} options + * @returns + */ + async openFeedForCollection(userId, collectionExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -226,7 +240,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) @@ -234,7 +248,14 @@ class RssFeedManager { return feed } - async openFeedForSeries(user, seriesExpanded, options) { + /** + * + * @param {string} userId + * @param {*} seriesExpanded + * @param {*} options + * @returns + */ + async openFeedForSeries(userId, seriesExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -242,7 +263,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) diff --git a/server/models/User.js b/server/models/User.js index 54cfca5e..ce1a419b 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -419,8 +419,11 @@ class User extends Model { ) } + get isRoot() { + return this.type === 'root' + } get isAdminOrUp() { - return this.type === 'root' || this.type === 'admin' + return this.isRoot || this.type === 'admin' } get isUser() { return this.type === 'user' diff --git a/server/objects/user/User.js b/server/objects/user/User.js index e76c3ac0..7608ca1b 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -372,88 +372,5 @@ class User { return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON } - - /** - * Get first available library id for user - * - * @param {string[]} libraryIds - * @returns {string|null} - */ - getDefaultLibraryId(libraryIds) { - // Libraries should already be in ascending display order, find first accessible - return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null - } - - getMediaProgress(libraryItemId, episodeId = null) { - if (!this.mediaProgress) return null - return this.mediaProgress.find((lip) => { - if (episodeId && lip.episodeId !== episodeId) return false - return lip.libraryItemId === libraryItemId - }) - } - - getAllMediaProgressForLibraryItem(libraryItemId) { - if (!this.mediaProgress) return [] - return this.mediaProgress.filter((li) => li.libraryItemId === libraryItemId) - } - - createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) { - const itemProgress = this.mediaProgress.find((li) => { - if (episodeId && li.episodeId !== episodeId) return false - return li.libraryItemId === libraryItem.id - }) - if (!itemProgress) { - const newItemProgress = new MediaProgress() - - newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id) - this.mediaProgress.push(newItemProgress) - return true - } - const wasUpdated = itemProgress.update(updatePayload) - - if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync - return wasUpdated - } - - checkCanAccessLibrary(libraryId) { - if (this.permissions.accessAllLibraries) return true - if (!this.librariesAccessible) return false - return this.librariesAccessible.includes(libraryId) - } - - checkCanAccessLibraryItemWithTags(tags) { - if (this.permissions.accessAllTags) return true - if (this.permissions.selectedTagsNotAccessible) { - if (!tags?.length) return true - return tags.every((tag) => !this.itemTagsSelected.includes(tag)) - } - if (!tags?.length) return false - return this.itemTagsSelected.some((tag) => tags.includes(tag)) - } - - checkCanAccessLibraryItem(libraryItem) { - if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false - - if (libraryItem.media.metadata.explicit && !this.canAccessExplicitContent) return false - return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags) - } - - /** - * Number of podcast episodes not finished for library item - * Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance - * @param {LibraryItem|object} libraryItem - * @returns {number} - */ - getNumEpisodesIncompleteForPodcast(libraryItem) { - if (!libraryItem?.media.episodes) return 0 - let numEpisodesIncomplete = 0 - for (const episode of libraryItem.media.episodes) { - const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id) - if (!mediaProgress?.isFinished) { - numEpisodesIncomplete++ - } - } - return numEpisodesIncomplete - } } module.exports = User diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 283c2417..291c24d6 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -39,6 +39,7 @@ class ApiRouter { constructor(Server) { /** @type {import('../Auth')} */ this.auth = Server.auth + /** @type {import('../managers/PlaybackSessionManager')} */ this.playbackSessionManager = Server.playbackSessionManager /** @type {import('../managers/AbMergeManager')} */ this.abMergeManager = Server.abMergeManager @@ -50,8 +51,10 @@ class ApiRouter { this.podcastManager = Server.podcastManager /** @type {import('../managers/AudioMetadataManager')} */ this.audioMetadataManager = Server.audioMetadataManager + /** @type {import('../managers/RssFeedManager')} */ this.rssFeedManager = Server.rssFeedManager this.cronManager = Server.cronManager + /** @type {import('../managers/NotificationManager')} */ this.notificationManager = Server.notificationManager this.emailManager = Server.emailManager this.apiCacheManager = Server.apiCacheManager @@ -281,7 +284,6 @@ class ApiRouter { this.router.get('/search/podcast', SearchController.findPodcasts.bind(this)) this.router.get('/search/authors', SearchController.findAuthor.bind(this)) this.router.get('/search/chapters', SearchController.findChapters.bind(this)) - this.router.get('/search/tracks', SearchController.findMusicTrack.bind(this)) // // Cache Routes (Admin and up)