Merge master

This commit is contained in:
advplyr 2023-11-26 13:54:06 -06:00
commit 36e00e8d6a
29 changed files with 614 additions and 39 deletions

View file

@ -566,6 +566,69 @@ class Auth {
Source: global.Source
}
}
/**
*
* @param {string} password
* @param {*} user
* @returns {boolean}
*/
comparePassword(password, user) {
if (user.type === 'root' && !password && !user.pash) return true
if (!password || !user.pash) return false
return bcrypt.compare(password, user.pash)
}
/**
* User changes their password from request
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async userChangePassword(req, res) {
let { password, newPassword } = req.body
newPassword = newPassword || ''
const matchingUser = req.user
// Only root can have an empty password
if (matchingUser.type !== 'root' && !newPassword) {
return res.json({
error: 'Invalid new password - Only root can have an empty password'
})
}
// Check password match
const compare = await this.comparePassword(password, matchingUser)
if (!compare) {
return res.json({
error: 'Invalid password'
})
}
let pw = ''
if (newPassword) {
pw = await this.hashPass(newPassword)
if (!pw) {
return res.json({
error: 'Hash failed'
})
}
}
matchingUser.pash = pw
const success = await Database.updateUser(matchingUser)
if (success) {
Logger.info(`[Auth] User "${matchingUser.username}" changed password`)
res.json({
success: true
})
} else {
res.json({
error: 'Unknown error'
})
}
}
}
module.exports = Auth

View file

@ -5,6 +5,7 @@ class Logger {
constructor() {
this.isDev = process.env.NODE_ENV !== 'production'
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
this.hideDevLogs = process.env.HIDE_DEV_LOGS === undefined ? !this.isDev : process.env.HIDE_DEV_LOGS === '1'
this.socketListeners = []
this.logManager = null
@ -92,7 +93,7 @@ class Logger {
* @param {...any} args
*/
dev(...args) {
if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return
if (this.hideDevLogs) return
console.log(`[${this.timestamp}] DEV:`, ...args)
}

View file

@ -32,13 +32,13 @@ const PodcastManager = require('./managers/PodcastManager')
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager')
const CronManager = require('./managers/CronManager')
const ApiCacheManager = require('./managers/ApiCacheManager')
const LibraryScanner = require('./scanner/LibraryScanner')
//Import the main Passport and Express-Session library
const passport = require('passport')
const expressSession = require('express-session')
class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
this.Port = PORT
@ -73,6 +73,7 @@ class Server {
this.audioMetadataManager = new AudioMetadataMangaer()
this.rssFeedManager = new RssFeedManager()
this.cronManager = new CronManager(this.podcastManager)
this.apiCacheManager = new ApiCacheManager()
// Routers
this.apiRouter = new ApiRouter(this)
@ -117,6 +118,7 @@ class Server {
const libraries = await Database.libraryModel.getAllOldLibraries()
await this.cronManager.init(libraries)
this.apiCacheManager.init()
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
@ -138,11 +140,13 @@ class Server {
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
* @see https://ionicframework.com/docs/troubleshooting/cors
*
* Running in development allows cors to allow testing the mobile apps in the browser
*/
app.use((req, res, next) => {
if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) {
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (allowedOrigins.some(o => o === req.get('origin'))) {
if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) {
res.header('Access-Control-Allow-Origin', req.get('origin'))
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', '*')

View file

@ -192,9 +192,9 @@ class SocketAuthority {
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
// Update user lastSeen
// Update user lastSeen without firing sequelize bulk update hooks
user.lastSeen = Date.now()
await Database.updateUser(user)
await Database.userModel.updateFromOld(user, false)
const initialPayload = {
userId: client.user.id,

View file

@ -0,0 +1,54 @@
const { LRUCache } = require('lru-cache')
const Logger = require('../Logger')
const Database = require('../Database')
class ApiCacheManager {
defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => (item.body.length + JSON.stringify(item.headers).length) }
defaultTtlOptions = { ttl: 30 * 60 * 1000 }
constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) {
this.cache = cache
this.ttlOptions = ttlOptions
}
init(database = Database) {
let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy']
hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook)))
}
clear(model, hook) {
Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`)
this.cache.clear()
}
get middleware() {
return (req, res, next) => {
const key = { user: req.user.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)
if (cached) {
Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`)
res.set(cached.headers)
res.status(cached.statusCode)
res.send(cached.body)
return
}
res.originalSend = res.send
res.send = (body) => {
Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`)
const cached = { body, headers: res.getHeaders(), statusCode: res.statusCode }
if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) {
Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`)
this.cache.set(stringifiedKey, cached, this.ttlOptions)
} else {
this.cache.set(stringifiedKey, cached)
}
res.originalSend(body)
}
next()
}
}
}
module.exports = ApiCacheManager

View file

@ -99,11 +99,13 @@ class User extends Model {
* Update User from old user model
*
* @param {oldUser} oldUser
* @param {boolean} [hooks=true] Run before / after bulk update hooks?
* @returns {Promise<boolean>}
*/
static updateFromOld(oldUser) {
static updateFromOld(oldUser, hooks = true) {
const user = this.getFromOld(oldUser)
return this.update(user, {
hooks: !!hooks,
where: {
id: user.id
}

View file

@ -48,6 +48,7 @@ class ApiRouter {
this.cronManager = Server.cronManager
this.notificationManager = Server.notificationManager
this.emailManager = Server.emailManager
this.apiCacheManager = Server.apiCacheManager
this.router = express()
this.router.disable('x-powered-by')
@ -58,6 +59,7 @@ class ApiRouter {
//
// Library Routes
//
this.router.get(/^\/libraries/, this.apiCacheManager.middleware)
this.router.post('/libraries', LibraryController.create.bind(this))
this.router.get('/libraries', LibraryController.findAll.bind(this))
this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this))