From 354e16e462731c7b415cdca0dd0e4704f38fbf11 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 22 Jul 2023 15:32:20 -0500 Subject: [PATCH] Update:Only load Users when needed --- server/Auth.js | 22 +++-- server/Database.js | 35 ++++---- server/Server.js | 3 +- server/controllers/SessionController.js | 6 +- server/controllers/UserController.js | 26 +++--- server/models/User.js | 102 +++++++++++++++++++++++- server/routers/ApiRouter.js | 7 +- 7 files changed, 151 insertions(+), 50 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 44a9317e..37ea4bb1 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -32,12 +32,13 @@ class Auth { await Database.updateServerSettings() // New token secret creation added in v2.1.0 so generate new API tokens for each user - if (Database.users.length) { - for (const user of Database.users) { + const users = await Database.models.user.getOldUsers() + if (users.length) { + for (const user of users) { user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) } - await Database.updateBulkUsers(Database.users) + await Database.updateBulkUsers(users) } } @@ -93,13 +94,18 @@ class Auth { verifyToken(token) { return new Promise((resolve) => { - jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => { + jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => { if (!payload || err) { Logger.error('JWT Verify Token Failed', err) return resolve(null) } - const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username) - resolve(user || null) + + const user = await Database.models.user.getUserByIdOrOldId(payload.userId) + if (user && user.username === payload.username) { + resolve(user) + } else { + resolve(null) + } }) }) } @@ -125,7 +131,7 @@ class Auth { const username = (req.body.username || '').toLowerCase() const password = req.body.password || '' - const user = Database.users.find(u => u.username.toLowerCase() === username) + const user = await Database.models.user.getUserByUsername(username) if (!user?.isActive) { Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) @@ -172,7 +178,7 @@ class Auth { async userChangePassword(req, res) { var { password, newPassword } = req.body newPassword = newPassword || '' - const matchingUser = Database.users.find(u => u.id === req.user.id) + const matchingUser = await Database.models.user.getUserById(req.user.id) // Only root can have an empty password if (matchingUser.type !== 'root' && !newPassword) { diff --git a/server/Database.js b/server/Database.js index 94ede654..7e407356 100644 --- a/server/Database.js +++ b/server/Database.js @@ -6,17 +6,18 @@ const fs = require('./libs/fsExtra') const Logger = require('./Logger') const dbMigration = require('./utils/migrations/dbMigration') +const Auth = require('./Auth') class Database { constructor() { this.sequelize = null this.dbPath = null this.isNew = false // New absdatabase.sqlite created + this.hasRootUser = false // Used to show initialization page in web ui // Temporarily using format of old DB // TODO: below data should be loaded from the DB as needed this.libraryItems = [] - this.users = [] this.settings = [] this.collections = [] this.playlists = [] @@ -32,10 +33,6 @@ class Database { return this.sequelize?.models || {} } - get hasRootUser() { - return this.users.some(u => u.type === 'root') - } - async checkHasDb() { if (!await fs.pathExists(this.dbPath)) { Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`) @@ -164,9 +161,6 @@ class Database { this.libraryItems = await this.models.libraryItem.loadAllLibraryItems() Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`) - this.users = await this.models.user.getOldUsers() - Logger.info(`[Database] Loaded ${this.users.length} users`) - this.collections = await this.models.collection.getOldCollections() Logger.info(`[Database] Loaded ${this.collections.length} collections`) @@ -179,6 +173,9 @@ class Database { this.series = await this.models.series.getAllOldSeries() Logger.info(`[Database] Loaded ${this.series.length} series`) + // Set if root user has been created + this.hasRootUser = await this.models.user.getHasRootUser() + Logger.info(`[Database] Db data loaded in ${((Date.now() - startTime) / 1000).toFixed(2)}s`) if (packageJson.version !== this.serverSettings.version) { @@ -186,18 +183,20 @@ class Database { this.serverSettings.version = packageJson.version await this.updateServerSettings() } - - this.models.library.getMaxDisplayOrder() } - async createRootUser(username, pash, token) { + /** + * Create root user + * @param {string} username + * @param {string} pash + * @param {Auth} auth + * @returns {boolean} true if created + */ + async createRootUser(username, pash, auth) { if (!this.sequelize) return false - const newUser = await this.models.user.createRootUser(username, pash, token) - if (newUser) { - this.users.push(newUser) - return true - } - return false + await this.models.user.createRootUser(username, pash, auth) + this.hasRootUser = true + return true } updateServerSettings() { @@ -214,7 +213,6 @@ class Database { async createUser(oldUser) { if (!this.sequelize) return false await this.models.user.createFromOld(oldUser) - this.users.push(oldUser) return true } @@ -231,7 +229,6 @@ class Database { async removeUser(userId) { if (!this.sequelize) return false await this.models.user.removeById(userId) - this.users = this.users.filter(u => u.id !== userId) } upsertMediaProgress(oldMediaProgress) { diff --git a/server/Server.js b/server/Server.js index 54e4a0b0..432c1b3f 100644 --- a/server/Server.js +++ b/server/Server.js @@ -250,7 +250,8 @@ class Server { // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist async cleanUserData() { - for (const _user of Database.users) { + const users = await Database.models.user.getOldUsers() + for (const _user of users) { if (_user.mediaProgress.length) { for (const mediaProgress of _user.mediaProgress) { const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId) diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 92dff559..698f58d7 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -43,17 +43,17 @@ class SessionController { res.json(payload) } - getOpenSessions(req, 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}"`) return res.sendStatus(404) } + const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects() const openSessions = this.playbackSessionManager.sessions.map(se => { - const user = Database.users.find(u => u.id === se.userId) || null return { ...se.toJSON(), - user: user ? { id: user.id, username: user.username } : null + user: minifiedUserObjects.find(u => u.id === se.userId) || null } }) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 5945637b..a92fdbe1 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -17,7 +17,8 @@ class UserController { const includes = (req.query.include || '').split(',').map(i => i.trim()) // Minimal toJSONForBrowser does not include mediaProgress and bookmarks - const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true)) + const allUsers = await Database.models.user.getOldUsers() + const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true)) if (includes.includes('latestSession')) { for (const user of users) { @@ -31,25 +32,20 @@ class UserController { }) } - findOne(req, res) { + async findOne(req, res) { if (!req.user.isAdminOrUp) { Logger.error('User other than admin attempting to get user', req.user) return res.sendStatus(403) } - const user = Database.users.find(u => u.id === req.params.id) - if (!user) { - return res.sendStatus(404) - } - - res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot)) + res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot)) } async create(req, res) { - var account = req.body + const account = req.body + const username = account.username - var username = account.username - var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + const usernameExists = await Database.models.user.getUserByUsername(username) if (usernameExists) { return res.status(500).send('Username already taken') } @@ -73,7 +69,7 @@ class UserController { } async update(req, res) { - var user = req.reqUser + const user = req.reqUser if (user.type === 'root' && !req.user.isRoot) { Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username) @@ -84,7 +80,7 @@ class UserController { var shouldUpdateToken = false if (account.username !== undefined && account.username !== user.username) { - var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase()) + const usernameExists = await Database.models.user.getUserByUsername(account.username) if (usernameExists) { return res.status(500).send('Username already taken') } @@ -178,7 +174,7 @@ class UserController { }) } - middleware(req, res, next) { + async 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) { @@ -186,7 +182,7 @@ class UserController { } if (req.params.id) { - req.reqUser = Database.users.find(u => u.id === req.params.id) + req.reqUser = await Database.models.user.getUserById(req.params.id) if (!req.reqUser) { return res.sendStatus(404) } diff --git a/server/models/User.js b/server/models/User.js index 32b2b436..6d461110 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,10 +1,14 @@ const uuidv4 = require("uuid").v4 -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, Op } = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') module.exports = (sequelize) => { class User extends Model { + /** + * Get all oldUsers + * @returns {Promise} + */ static async getOldUsers() { const users = await this.findAll({ include: sequelize.models.mediaProgress @@ -89,6 +93,13 @@ module.exports = (sequelize) => { }) } + /** + * Create root user + * @param {string} username + * @param {string} pash + * @param {Auth} auth + * @returns {oldUser} + */ static async createRootUser(username, pash, auth) { const userId = uuidv4() @@ -106,6 +117,95 @@ module.exports = (sequelize) => { await this.createFromOld(newRoot) return newRoot } + + /** + * Get a user by id or by the old database id + * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id + * @param {string} userId + * @returns {Promise} null if not found + */ + static async getUserByIdOrOldId(userId) { + if (!userId) return null + const user = await this.findOne({ + where: { + [Op.or]: [ + { + id: userId + }, + { + extraData: { + [Op.substring]: userId + } + } + ] + }, + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by username case insensitive + * @param {string} username + * @returns {Promise} returns null if not found + */ + static async getUserByUsername(username) { + if (!username) return null + const user = await this.findOne({ + where: { + username: { + [Op.like]: username + } + }, + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by id + * @param {string} userId + * @returns {Promise} returns null if not found + */ + static async getUserById(userId) { + if (!userId) return null + const user = await this.findByPk(userId, { + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get array of user id and username + * @returns {object[]} { id, username } + */ + static async getMinifiedUserObjects() { + const users = await this.findAll({ + attributes: ['id', 'username'] + }) + return users.map(u => { + return { + id: u.id, + username: u.username + } + }) + } + + /** + * Return true if root user exists + * @returns {boolean} + */ + static async getHasRootUser() { + const count = await this.count({ + where: { + type: 'root' + } + }) + return count > 0 + } } User.init({ diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 0324891a..9db034ff 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -381,7 +381,8 @@ class ApiRouter { async handleDeleteLibraryItem(libraryItem) { // Remove media progress for this library item from all users - for (const user of Database.users) { + const users = await Database.models.user.getOldUsers() + for (const user of users) { for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) { await Database.removeMediaProgress(mediaProgress.id) } @@ -462,11 +463,11 @@ class ApiRouter { async getAllSessionsWithUserData() { const sessions = await Database.getPlaybackSessions() sessions.sort((a, b) => b.updatedAt - a.updatedAt) + const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects() return sessions.map(se => { - const user = Database.users.find(u => u.id === se.userId) return { ...se, - user: user ? { id: user.id, username: user.username } : null + user: minifiedUserObjects.find(u => u.id === se.userId) || null } }) }