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()
// 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) {

View File

@ -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,19 +183,21 @@ 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)
await this.models.user.createRootUser(username, pash, auth)
this.hasRootUser = true
return true
}
return false
}
updateServerSettings() {
if (!this.sequelize) return false
@ -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) {

View File

@ -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)

View File

@ -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
}
})

View File

@ -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)
}

View File

@ -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<oldUser>}
*/
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<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({

View File

@ -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
}
})
}