Update:Only load Users when needed

This commit is contained in:
advplyr 2023-07-22 15:32:20 -05:00
parent 1d974375a0
commit 354e16e462
7 changed files with 151 additions and 50 deletions

View File

@ -32,12 +32,13 @@ class Auth {
await Database.updateServerSettings() await Database.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user // New token secret creation added in v2.1.0 so generate new API tokens for each user
if (Database.users.length) { const users = await Database.models.user.getOldUsers()
for (const user of Database.users) { if (users.length) {
for (const user of users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) 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`) 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) { verifyToken(token) {
return new Promise((resolve) => { return new Promise((resolve) => {
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => { jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => {
if (!payload || err) { if (!payload || err) {
Logger.error('JWT Verify Token Failed', err) Logger.error('JWT Verify Token Failed', err)
return resolve(null) 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 username = (req.body.username || '').toLowerCase()
const password = req.body.password || '' 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) { if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) 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) { async userChangePassword(req, res) {
var { password, newPassword } = req.body var { password, newPassword } = req.body
newPassword = newPassword || '' 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 // Only root can have an empty password
if (matchingUser.type !== 'root' && !newPassword) { if (matchingUser.type !== 'root' && !newPassword) {

View File

@ -6,17 +6,18 @@ const fs = require('./libs/fsExtra')
const Logger = require('./Logger') const Logger = require('./Logger')
const dbMigration = require('./utils/migrations/dbMigration') const dbMigration = require('./utils/migrations/dbMigration')
const Auth = require('./Auth')
class Database { class Database {
constructor() { constructor() {
this.sequelize = null this.sequelize = null
this.dbPath = null this.dbPath = null
this.isNew = false // New absdatabase.sqlite created this.isNew = false // New absdatabase.sqlite created
this.hasRootUser = false // Used to show initialization page in web ui
// Temporarily using format of old DB // Temporarily using format of old DB
// TODO: below data should be loaded from the DB as needed // TODO: below data should be loaded from the DB as needed
this.libraryItems = [] this.libraryItems = []
this.users = []
this.settings = [] this.settings = []
this.collections = [] this.collections = []
this.playlists = [] this.playlists = []
@ -32,10 +33,6 @@ class Database {
return this.sequelize?.models || {} return this.sequelize?.models || {}
} }
get hasRootUser() {
return this.users.some(u => u.type === 'root')
}
async checkHasDb() { async checkHasDb() {
if (!await fs.pathExists(this.dbPath)) { if (!await fs.pathExists(this.dbPath)) {
Logger.info(`[Database] absdatabase.sqlite not found at ${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() this.libraryItems = await this.models.libraryItem.loadAllLibraryItems()
Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`) 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() this.collections = await this.models.collection.getOldCollections()
Logger.info(`[Database] Loaded ${this.collections.length} collections`) Logger.info(`[Database] Loaded ${this.collections.length} collections`)
@ -179,6 +173,9 @@ class Database {
this.series = await this.models.series.getAllOldSeries() this.series = await this.models.series.getAllOldSeries()
Logger.info(`[Database] Loaded ${this.series.length} series`) 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`) Logger.info(`[Database] Db data loaded in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
if (packageJson.version !== this.serverSettings.version) { if (packageJson.version !== this.serverSettings.version) {
@ -186,18 +183,20 @@ class Database {
this.serverSettings.version = packageJson.version this.serverSettings.version = packageJson.version
await this.updateServerSettings() 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 if (!this.sequelize) return false
const newUser = await this.models.user.createRootUser(username, pash, token) await this.models.user.createRootUser(username, pash, auth)
if (newUser) { this.hasRootUser = true
this.users.push(newUser) return true
return true
}
return false
} }
updateServerSettings() { updateServerSettings() {
@ -214,7 +213,6 @@ class Database {
async createUser(oldUser) { async createUser(oldUser) {
if (!this.sequelize) return false if (!this.sequelize) return false
await this.models.user.createFromOld(oldUser) await this.models.user.createFromOld(oldUser)
this.users.push(oldUser)
return true return true
} }
@ -231,7 +229,6 @@ class Database {
async removeUser(userId) { async removeUser(userId) {
if (!this.sequelize) return false if (!this.sequelize) return false
await this.models.user.removeById(userId) await this.models.user.removeById(userId)
this.users = this.users.filter(u => u.id !== userId)
} }
upsertMediaProgress(oldMediaProgress) { upsertMediaProgress(oldMediaProgress) {

View File

@ -250,7 +250,8 @@ class Server {
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
async cleanUserData() { async cleanUserData() {
for (const _user of Database.users) { const users = await Database.models.user.getOldUsers()
for (const _user of users) {
if (_user.mediaProgress.length) { if (_user.mediaProgress.length) {
for (const mediaProgress of _user.mediaProgress) { for (const mediaProgress of _user.mediaProgress) {
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId) const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)

View File

@ -43,17 +43,17 @@ class SessionController {
res.json(payload) res.json(payload)
} }
getOpenSessions(req, res) { async getOpenSessions(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`) Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
const openSessions = this.playbackSessionManager.sessions.map(se => { const openSessions = this.playbackSessionManager.sessions.map(se => {
const user = Database.users.find(u => u.id === se.userId) || null
return { return {
...se.toJSON(), ...se.toJSON(),
user: user ? { id: user.id, username: user.username } : null user: minifiedUserObjects.find(u => u.id === se.userId) || null
} }
}) })

View File

@ -17,7 +17,8 @@ class UserController {
const includes = (req.query.include || '').split(',').map(i => i.trim()) const includes = (req.query.include || '').split(',').map(i => i.trim())
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks // 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')) { if (includes.includes('latestSession')) {
for (const user of users) { for (const user of users) {
@ -31,25 +32,20 @@ class UserController {
}) })
} }
findOne(req, res) { async findOne(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user) Logger.error('User other than admin attempting to get user', req.user)
return res.sendStatus(403) return res.sendStatus(403)
} }
const user = Database.users.find(u => u.id === req.params.id) res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
if (!user) {
return res.sendStatus(404)
}
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
} }
async create(req, res) { async create(req, res) {
var account = req.body const account = req.body
const username = account.username
var username = account.username const usernameExists = await Database.models.user.getUserByUsername(username)
var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
if (usernameExists) { if (usernameExists) {
return res.status(500).send('Username already taken') return res.status(500).send('Username already taken')
} }
@ -73,7 +69,7 @@ class UserController {
} }
async update(req, res) { async update(req, res) {
var user = req.reqUser const user = req.reqUser
if (user.type === 'root' && !req.user.isRoot) { if (user.type === 'root' && !req.user.isRoot) {
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username) Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
@ -84,7 +80,7 @@ class UserController {
var shouldUpdateToken = false var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) { 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) { if (usernameExists) {
return res.status(500).send('Username already taken') 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) { if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403) 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.user.isAdminOrUp) {
@ -186,7 +182,7 @@ class UserController {
} }
if (req.params.id) { 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) { if (!req.reqUser) {
return res.sendStatus(404) return res.sendStatus(404)
} }

View File

@ -1,10 +1,14 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require("uuid").v4
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model, Op } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
const oldUser = require('../objects/user/User') const oldUser = require('../objects/user/User')
module.exports = (sequelize) => { module.exports = (sequelize) => {
class User extends Model { class User extends Model {
/**
* Get all oldUsers
* @returns {Promise<oldUser>}
*/
static async getOldUsers() { static async getOldUsers() {
const users = await this.findAll({ const users = await this.findAll({
include: sequelize.models.mediaProgress 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) { static async createRootUser(username, pash, auth) {
const userId = uuidv4() const userId = uuidv4()
@ -106,6 +117,95 @@ module.exports = (sequelize) => {
await this.createFromOld(newRoot) await this.createFromOld(newRoot)
return 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<oldUser|null>} 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<oldUser|null>} 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<oldUser|null>} 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({ User.init({

View File

@ -381,7 +381,8 @@ class ApiRouter {
async handleDeleteLibraryItem(libraryItem) { async handleDeleteLibraryItem(libraryItem) {
// Remove media progress for this library item from all users // 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)) { for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) {
await Database.removeMediaProgress(mediaProgress.id) await Database.removeMediaProgress(mediaProgress.id)
} }
@ -462,11 +463,11 @@ class ApiRouter {
async getAllSessionsWithUserData() { async getAllSessionsWithUserData() {
const sessions = await Database.getPlaybackSessions() const sessions = await Database.getPlaybackSessions()
sessions.sort((a, b) => b.updatedAt - a.updatedAt) sessions.sort((a, b) => b.updatedAt - a.updatedAt)
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
return sessions.map(se => { return sessions.map(se => {
const user = Database.users.find(u => u.id === se.userId)
return { return {
...se, ...se,
user: user ? { id: user.id, username: user.username } : null user: minifiedUserObjects.find(u => u.id === se.userId) || null
} }
}) })
} }