refactor: init UserError, a custom Error object

This will be used for errors that are to be delivered to users, AND not
to be logged into the server (as in it stacktraces and all).
This will eventually remove the need to throw string literals.

In this commit, this has only been implemented on albumsController.js,
but more will soon to come.
This commit is contained in:
Bobby Wibowo 2021-01-08 07:29:14 +07:00
parent 0dfdccb25e
commit ae31033c0c
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
3 changed files with 147 additions and 113 deletions

View File

@ -7,6 +7,8 @@ const paths = require('./pathsController')
const perms = require('./permissionController') const perms = require('./permissionController')
const uploadController = require('./uploadController') const uploadController = require('./uploadController')
const utils = require('./utilsController') const utils = require('./utilsController')
const apiErrorsHandler = require('./handlers/apiErrorsHandler.js')
const UserError = require('./utils/UserError')
const config = require('./../config') const config = require('./../config')
const logger = require('./../logger') const logger = require('./../logger')
const db = require('knex')(config.database) const db = require('knex')(config.database)
@ -66,28 +68,28 @@ self.getUniqueRandomName = async () => {
return identifier return identifier
} }
throw 'Sorry, we could not allocate a unique random identifier. Try again?' throw new UserError('Failed to allocate a unique identifier for the album. Try again?', 500)
} }
self.list = async (req, res, next) => { self.list = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const all = req.headers.all === '1'
const simple = req.headers.simple
const ismoderator = perms.is(user, 'moderator')
if (all && !ismoderator) return res.status(403).end()
const filter = function () {
if (!all) {
this.where({
enabled: 1,
userid: user.id
})
}
}
try { try {
const user = await utils.authorize(req, res)
if (!user) return
const all = req.headers.all === '1'
const simple = req.headers.simple
const ismoderator = perms.is(user, 'moderator')
if (all && !ismoderator) return res.status(403).end()
const filter = function () {
if (!all) {
this.where({
enabled: 1,
userid: user.id
})
}
}
// Query albums count for pagination // Query albums count for pagination
const count = await db.table('albums') const count = await db.table('albums')
.where(filter) .where(filter)
@ -164,22 +166,21 @@ self.list = async (req, res, next) => {
return res.json({ success: true, albums, count, users, homeDomain }) return res.json({ success: true, albums, count, users, homeDomain })
} catch (error) { } catch (error) {
logger.error(error) return apiErrorsHandler(error, req, res, next)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
} }
} }
self.create = async (req, res, next) => { self.create = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const name = typeof req.body.name === 'string'
? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength))
: ''
if (!name) return res.json({ success: false, description: 'No album name specified.' })
try { try {
const user = await utils.authorize(req, res)
if (!user) return
const name = typeof req.body.name === 'string'
? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength))
: ''
if (!name) return res.json({ success: false, description: 'No album name specified.' })
const album = await db.table('albums') const album = await db.table('albums')
.where({ .where({
name, name,
@ -211,8 +212,7 @@ self.create = async (req, res, next) => {
return res.json({ success: true, id: ids[0] }) return res.json({ success: true, id: ids[0] })
} catch (error) { } catch (error) {
logger.error(error) return apiErrorsHandler(error, req, res, next)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
} }
} }
@ -222,14 +222,14 @@ self.delete = async (req, res, next) => {
} }
self.disable = async (req, res, next) => { self.disable = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const id = req.body.id
const purge = req.body.purge
if (!Number.isFinite(id)) return res.json({ success: false, description: 'No album specified.' })
try { try {
const user = await utils.authorize(req, res)
if (!user) return
const id = req.body.id
const purge = req.body.purge
if (!Number.isFinite(id)) return res.json({ success: false, description: 'No album specified.' })
if (purge) { if (purge) {
const files = await db.table('files') const files = await db.table('files')
.where({ .where({
@ -264,43 +264,39 @@ self.disable = async (req, res, next) => {
.then(row => row.identifier) .then(row => row.identifier)
await paths.unlink(path.join(paths.zips, `${identifier}.zip`)) await paths.unlink(path.join(paths.zips, `${identifier}.zip`))
return res.json({ success: true })
} catch (error) { } catch (error) {
if (error && error.code !== 'ENOENT') { return apiErrorsHandler(error, req, res, next)
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
}
} }
return res.json({ success: true })
} }
self.edit = async (req, res, next) => { self.edit = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const ismoderator = perms.is(user, 'moderator')
const id = parseInt(req.body.id)
if (isNaN(id)) return res.json({ success: false, description: 'No album specified.' })
const name = typeof req.body.name === 'string'
? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength))
: ''
if (!name) return res.json({ success: false, description: 'No name specified.' })
const filter = function () {
this.where('id', id)
if (!ismoderator) {
this.andWhere({
enabled: 1,
userid: user.id
})
}
}
try { try {
const user = await utils.authorize(req, res)
if (!user) return
const ismoderator = perms.is(user, 'moderator')
const id = parseInt(req.body.id)
if (isNaN(id)) return res.json({ success: false, description: 'No album specified.' })
const name = typeof req.body.name === 'string'
? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength))
: ''
if (!name) return res.json({ success: false, description: 'No name specified.' })
const filter = function () {
this.where('id', id)
if (!ismoderator) {
this.andWhere({
enabled: 1,
userid: user.id
})
}
}
const album = await db.table('albums') const album = await db.table('albums')
.where(filter) .where(filter)
.first() .first()
@ -358,8 +354,7 @@ self.edit = async (req, res, next) => {
return res.json({ success: true, name }) return res.json({ success: true, name })
} }
} catch (error) { } catch (error) {
logger.error(error) return apiErrorsHandler(error, req, res, next)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
} }
} }
@ -370,12 +365,12 @@ self.rename = async (req, res, next) => {
} }
self.get = async (req, res, next) => { self.get = async (req, res, next) => {
const identifier = req.params.identifier
if (identifier === undefined) {
return res.status(401).json({ success: false, description: 'No identifier provided.' })
}
try { try {
const identifier = req.params.identifier
if (identifier === undefined) {
return res.status(401).json({ success: false, description: 'No identifier provided.' })
}
const album = await db.table('albums') const album = await db.table('albums')
.where({ .where({
identifier, identifier,
@ -416,30 +411,29 @@ self.get = async (req, res, next) => {
files files
}) })
} catch (error) { } catch (error) {
logger.error(error) return apiErrorsHandler(error, req, res, next)
return res.status(500).json({ success: false, description: 'An unexpected error occcured. Try again?' })
} }
} }
self.generateZip = async (req, res, next) => { self.generateZip = async (req, res, next) => {
const versionString = parseInt(req.query.v)
const identifier = req.params.identifier
if (identifier === undefined) {
return res.status(401).json({
success: false,
description: 'No identifier provided.'
})
}
if (!config.uploads.generateZips) {
return res.status(401).json({
success: false,
description: 'ZIP generation disabled.'
})
}
try { try {
const versionString = parseInt(req.query.v)
const identifier = req.params.identifier
if (identifier === undefined) {
return res.status(401).json({
success: false,
description: 'No identifier provided.'
})
}
if (!config.uploads.generateZips) {
return res.status(401).json({
success: false,
description: 'ZIP generation disabled.'
})
}
const album = await db.table('albums') const album = await db.table('albums')
.where({ .where({
identifier, identifier,
@ -547,8 +541,7 @@ self.generateZip = async (req, res, next) => {
self.zipEmitters.get(identifier).emit('done', filePath, fileName) self.zipEmitters.get(identifier).emit('done', filePath, fileName)
return res.download(filePath, fileName) return res.download(filePath, fileName)
} catch (error) { } catch (error) {
logger.error(error) return apiErrorsHandler(error, req, res, next)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
} }
} }
@ -589,20 +582,21 @@ self.listFiles = async (req, res, next) => {
} }
self.addFiles = async (req, res, next) => { self.addFiles = async (req, res, next) => {
const user = await utils.authorize(req, res) let ids, albumid, failed, albumids
if (!user) return
const ids = req.body.ids
if (!Array.isArray(ids) || !ids.length) {
return res.json({ success: false, description: 'No files specified.' })
}
let albumid = parseInt(req.body.albumid)
if (isNaN(albumid) || albumid < 0) albumid = null
let failed = []
const albumids = []
try { try {
const user = await utils.authorize(req, res)
if (!user) return
ids = req.body.ids
if (!Array.isArray(ids) || !ids.length) {
return res.json({ success: false, description: 'No files specified.' })
}
albumid = parseInt(req.body.albumid)
if (isNaN(albumid) || albumid < 0) albumid = null
failed = []
albumids = []
if (albumid !== null) { if (albumid !== null) {
const album = await db.table('albums') const album = await db.table('albums')
.where('id', albumid) .where('id', albumid)
@ -646,14 +640,13 @@ self.addFiles = async (req, res, next) => {
return res.json({ success: true, failed }) return res.json({ success: true, failed })
} catch (error) { } catch (error) {
logger.error(error) if (Array.isArray(failed) && (failed.length === ids.length)) {
if (failed.length === ids.length) {
return res.json({ return res.json({
success: false, success: false,
description: `Could not ${albumid === null ? 'add' : 'remove'} any files ${albumid === null ? 'to' : 'from'} the album.` description: `Could not ${albumid === null ? 'add' : 'remove'} any files ${albumid === null ? 'to' : 'from'} the album.`
}) })
} else { } else {
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' }) return apiErrorsHandler(error, req, res, next)
} }
} }
} }

View File

@ -0,0 +1,30 @@
const UserError = require('./../utils/UserError')
const logger = require('./../../logger')
module.exports = (error, req, res, next) => {
if (!res) {
return logger.error(new Error('Missing "res" object.'))
}
// Intentional error messages to be delivered to users
const isUserError = error instanceof UserError
// ENOENT or missing file errors, typically harmless, so do not log stacktrace
const isENOENTError = error instanceof Error && error.code === 'ENOENT'
if (!isUserError && !isENOENTError) {
logger.error(error)
}
const statusCode = isUserError
? error.statusCode
: 500
const description = isUserError
? error.message
: 'An unexpected error occurred. Try again?'
return res
.status(statusCode)
.json({ success: false, description })
}

View File

@ -0,0 +1,11 @@
class UserError extends Error {
constructor (message, statusCode) {
super(message)
this.statusCode = statusCode !== undefined
? statusCode
: 400
}
}
module.exports = UserError