diff --git a/controllers/albumsController.js b/controllers/albumsController.js index c4dfba1..db7a520 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -102,18 +102,18 @@ self.unholdAlbumIdentifiers = res => { } self.list = async (req, res) => { - const user = await utils.authorize(req) - 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 ismoderator = perms.is(req.locals.user, 'moderator') + if (all && !ismoderator) { + return res.status(403).end() + } const filter = function () { if (!all) { this.where({ enabled: 1, - userid: user.id + userid: req.locals.user.id }) } } @@ -227,34 +227,32 @@ self.list = async (req, res) => { } self.create = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body - req.body = await req.json() - const name = typeof req.body.name === 'string' ? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength)) : '' - if (!name) throw new ClientError('No album name specified.') + if (!name) { + throw new ClientError('No album name specified.') + } const album = await utils.db.table('albums') .where({ name, enabled: 1, - userid: user.id + userid: req.locals.user.id }) .first() - if (album) throw new ClientError('Album name already in use.', { statusCode: 403 }) + if (album) { + throw new ClientError('Album name already in use.', { statusCode: 403 }) + } const identifier = await self.getUniqueAlbumIdentifier(res) const ids = await utils.db.table('albums').insert({ name, enabled: 1, - userid: user.id, + userid: req.locals.user.id, identifier, timestamp: Math.floor(Date.now() / 1000), editedAt: 0, @@ -272,39 +270,33 @@ self.create = async (req, res) => { } self.delete = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body and re-map for .disable() - req.body = await req.json() - .then(obj => { - obj.del = true - return obj - }) + // Re-map Request.body for .disable() + req.body.del = true return self.disable(req, res) } self.disable = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - const ismoderator = perms.is(user, 'moderator') - - // Parse POST body, if required - req.body = req.body || await req.json() + const ismoderator = perms.is(req.locals.user, 'moderator') const id = parseInt(req.body.id) - if (isNaN(id)) throw new ClientError('No album specified.') + if (isNaN(id)) { + throw new ClientError('No album specified.') + } const purge = req.body.purge + + // Only allow moderators to delete other users' albums const del = ismoderator ? req.body.del : false const filter = function () { this.where('id', id) + // Only allow moderators to disable other users' albums if (!ismoderator) { this.andWhere({ enabled: 1, - userid: user.id + userid: req.locals.user.id }) } } @@ -326,7 +318,7 @@ self.disable = async (req, res) => { if (files.length) { const ids = files.map(file => file.id) - const failed = await utils.bulkDeleteFromDb('id', ids, user) + const failed = await utils.bulkDeleteFromDb('id', ids, req.locals.user) if (failed.length) { return res.json({ success: false, failed }) } @@ -352,36 +344,38 @@ self.disable = async (req, res) => { await paths.unlink(path.join(paths.zips, `${album.identifier}.zip`)) } catch (error) { // Re-throw non-ENOENT error - if (error.code !== 'ENOENT') throw error + if (error.code !== 'ENOENT') { + throw error + } } return res.json({ success: true }) } self.edit = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - const ismoderator = perms.is(user, 'moderator') - - // Parse POST body, if required - req.body = req.body || await req.json() + const ismoderator = perms.is(req.locals.user, 'moderator') const id = parseInt(req.body.id) - if (isNaN(id)) throw new ClientError('No album specified.') + if (isNaN(id)) { + throw new ClientError('No album specified.') + } const name = typeof req.body.name === 'string' ? utils.escape(req.body.name.trim().substring(0, self.titleMaxLength)) : '' - if (!name) throw new ClientError('No album name specified.') + if (!name) { + throw new ClientError('No album name specified.') + } const filter = function () { this.where('id', id) + // Only allow moderators to edit other users' albums if (!ismoderator) { this.andWhere({ enabled: 1, - userid: user.id + userid: req.locals.user.id }) } } @@ -402,14 +396,14 @@ self.edit = async (req, res) => { .where({ name, enabled: 1, - userid: user.id + userid: req.locals.user.id }) .whereNot('id', id) .first() if ((album.enabled || (albumNewState === true)) && nameInUse) { - if (req._old) { - // Old rename API (stick with 200 status code for this) + if (req._legacy) { + // Legacy rename API (stick with 200 status code for this) throw new ClientError('You did not specify a new name.', { statusCode: 200 }) } else { throw new ClientError('Album name already in use.', { statusCode: 403 }) @@ -448,7 +442,9 @@ self.edit = async (req, res) => { await paths.rename(oldZip, newZip) } catch (error) { // Re-throw non-ENOENT error - if (error.code !== 'ENOENT') throw error + if (error.code !== 'ENOENT') { + throw error + } } return res.json({ @@ -461,16 +457,11 @@ self.edit = async (req, res) => { } self.rename = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body and re-map for .edit() - req.body = await req.json() - .then(obj => { - return { - _old: true, - name: obj.name - } - }) + // Re-map Request.body for .edit() + req.body = { + _legacy: true, + name: req.body.name + } return self.edit(req, res) } @@ -529,6 +520,7 @@ self.getUpstreamCompat = async (req, res) => { // map to .get() with chibisafe/upstream compatibility // This API is known to be used in Pitu/Magane req.locals.upstreamCompat = true + res._json = res.json res.json = (body = {}) => { // Rebuild JSON payload to match lolisafe upstream @@ -549,7 +541,10 @@ self.getUpstreamCompat = async (req, res) => { } }) - if (rebuild.message) rebuild.message = rebuild.message.replace(/\.$/, '') + if (rebuild.message) { + rebuild.message = rebuild.message.replace(/\.$/, '') + } + return res._json(rebuild) } @@ -593,7 +588,9 @@ self.generateZip = async (req, res) => { return } catch (error) { // Re-throw non-ENOENT error - if (error.code !== 'ENOENT') throw error + if (error.code !== 'ENOENT') { + throw error + } } } @@ -673,19 +670,15 @@ self.generateZip = async (req, res) => { } self.addFiles = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body - req.body = await req.json() - const ids = req.body.ids if (!Array.isArray(ids) || !ids.length) { throw new ClientError('No files specified.') } let albumid = parseInt(req.body.albumid) - if (isNaN(albumid) || albumid < 0) albumid = null + if (isNaN(albumid) || albumid < 0) { + albumid = null + } const failed = [] const albumids = [] @@ -694,8 +687,10 @@ self.addFiles = async (req, res) => { const album = await utils.db.table('albums') .where('id', albumid) .where(function () { - if (user.username !== 'root') { - this.where('userid', user.id) + // Only allow "root" user to arbitrarily add/remove files to/from any albums + // NOTE: Dashboard does not facilitate this, intended for manual API calls + if (req.locals.user.username !== 'root') { + this.where('userid', req.locals.user.id) } }) .first() @@ -709,7 +704,7 @@ self.addFiles = async (req, res) => { const files = await utils.db.table('files') .whereIn('id', ids) - .where('userid', user.id) + .where('userid', req.locals.user.id) failed.push(...ids.filter(id => !files.find(file => file.id === id))) diff --git a/controllers/authController.js b/controllers/authController.js index 72c15ac..10e22d8 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -35,27 +35,79 @@ const usersPerPage = config.dashboard ? Math.max(Math.min(config.dashboard.usersPerPage || 0, 100), 1) : 25 +self.assertUser = async (token, fields) => { + // Default fields/columns to fetch from database + const _fields = ['id', 'username', 'enabled', 'timestamp', 'permission', 'registration'] + + // Allow fetching additional fields/columns + if (typeof fields === 'string') { + fields = [fields] + } + if (Array.isArray(fields)) { + _fields.push(...fields) + } + + const user = await utils.db.table('users') + .where('token', token) + .select(_fields) + .first() + if (user) { + if (user.enabled === false || user.enabled === 0) { + throw new ClientError('This account has been disabled.', { statusCode: 403 }) + } + return user + } else { + throw new ClientError('Invalid token.', { statusCode: 403 }) + } +} + +// _ is next() if this was a synchronous middleware function +self.requireUser = async (req, res, _, fields) => { + // Throws when token is missing, thus use only for users-only routes + const token = req.headers.token + if (token === undefined) { + throw new ClientError('No token provided.', { statusCode: 403 }) + } + + // Add user data to Request.locals.user + req.locals.user = await self.assertUser(token, fields) +} + +// _ is next() if this was a synchronous middleware function +self.optionalUser = async (req, res, _, fields) => { + // Throws when token if missing only when private is set to true in config, + // thus use for routes that can handle no auth requests + const token = req.headers.token + if (token) { + // Add user data to Request.locals.user + req.locals.user = await self.assertUser(token, fields) + } else if (config.private === true) { + throw new ClientError('No token provided.', { statusCode: 403 }) + } +} + self.verify = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body - req.body = await req.json() - const username = typeof req.body.username === 'string' ? req.body.username.trim() : '' - if (!username) throw new ClientError('No username provided.') + if (!username) { + throw new ClientError('No username provided.') + } const password = typeof req.body.password === 'string' ? req.body.password.trim() : '' - if (!password) throw new ClientError('No password provided.') + if (!password) { + throw new ClientError('No password provided.') + } const user = await utils.db.table('users') .where('username', username) .first() - if (!user) throw new ClientError('Wrong credentials.', { statusCode: 403 }) + if (!user) { + throw new ClientError('Wrong credentials.', { statusCode: 403 }) + } if (user.enabled === false || user.enabled === 0) { throw new ClientError('This account has been disabled.', { statusCode: 403 }) @@ -70,11 +122,6 @@ self.verify = async (req, res) => { } self.register = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body - req.body = await req.json() - if (config.enableUserAccounts === false) { throw new ClientError('Registration is currently disabled.', { statusCode: 403 }) } @@ -119,12 +166,6 @@ self.register = async (req, res) => { } self.changePassword = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body - req.body = await req.json() - const password = typeof req.body.password === 'string' ? req.body.password.trim() : '' @@ -135,7 +176,7 @@ self.changePassword = async (req, res) => { const hash = await bcrypt.hash(password, saltRounds) await utils.db.table('users') - .where('id', user.id) + .where('id', req.locals.user.id) .update('password', hash) return res.json({ success: true }) @@ -152,14 +193,10 @@ self.assertPermission = (user, target) => { } self.createUser = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body - req.body = await req.json() - - const isadmin = perms.is(user, 'admin') - if (!isadmin) return res.status(403).end() + const isadmin = perms.is(req.locals.user, 'admin') + if (!isadmin) { + return res.status(403).end() + } const username = typeof req.body.username === 'string' ? req.body.username.trim() @@ -215,22 +252,22 @@ self.createUser = async (req, res) => { } self.editUser = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body, if required - req.body = req.body || await req.json() - - const isadmin = perms.is(user, 'admin') - if (!isadmin) throw new ClientError('', { statusCode: 403 }) + const isadmin = perms.is(req.locals.user, 'admin') + if (!isadmin) { + return res.status(403).end() + } const id = parseInt(req.body.id) - if (isNaN(id)) throw new ClientError('No user specified.') + if (isNaN(id)) { + throw new ClientError('No user specified.') + } const target = await utils.db.table('users') .where('id', id) .first() - self.assertPermission(user, target) + + // Ensure this user has permission to tamper with target user + self.assertPermission(req.locals.user, target) const update = {} @@ -272,38 +309,33 @@ self.editUser = async (req, res) => { } self.disableUser = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body and re-map for .editUser() - req.body = await req.json() - .then(obj => { - return { - id: obj.id, - enabled: false - } - }) + // Re-map Request.body for .editUser() + req.body = { + id: req.body.id, + enabled: false + } return self.editUser(req, res) } self.deleteUser = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body - req.body = await req.json() - - const isadmin = perms.is(user, 'admin') - if (!isadmin) throw new ClientError('', { statusCode: 403 }) + const isadmin = perms.is(req.locals.user, 'admin') + if (!isadmin) { + return res.status(403).end() + } const id = parseInt(req.body.id) const purge = req.body.purge - if (isNaN(id)) throw new ClientError('No user specified.') + if (isNaN(id)) { + throw new ClientError('No user specified.') + } const target = await utils.db.table('users') .where('id', id) .first() - self.assertPermission(user, target) + + // Ensure this user has permission to tamper with target user + self.assertPermission(req.locals.user, target) const files = await utils.db.table('files') .where('userid', id) @@ -312,7 +344,7 @@ self.deleteUser = async (req, res) => { if (files.length) { const fileids = files.map(file => file.id) if (purge) { - const failed = await utils.bulkDeleteFromDb('id', fileids, user) + const failed = await utils.bulkDeleteFromDb('id', fileids, req.locals.user) utils.invalidateStatsCache('uploads') if (failed.length) { return res.json({ success: false, failed }) @@ -361,10 +393,10 @@ self.bulkDeleteUsers = async (req, res) => { } self.listUsers = async (req, res) => { - const user = await utils.authorize(req) - - const isadmin = perms.is(user, 'admin') - if (!isadmin) throw new ClientError('', { statusCode: 403 }) + const isadmin = perms.is(req.locals.user, 'admin') + if (!isadmin) { + return res.status(403).end() + } // Base result object const result = { success: true, users: [], usersPerPage, count: 0 } diff --git a/controllers/tokenController.js b/controllers/tokenController.js index 5fb5b46..31a8fe3 100644 --- a/controllers/tokenController.js +++ b/controllers/tokenController.js @@ -63,16 +63,13 @@ self.unholdTokens = res => { } self.verify = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body - req.body = await req.json() - const token = typeof req.body.token === 'string' ? req.body.token.trim() : '' - if (!token) throw new ClientError('No token provided.', { statusCode: 403 }) + if (!token) { + throw new ClientError('No token provided.', { statusCode: 403 }) + } const user = await utils.db.table('users') .where('token', token) @@ -106,17 +103,14 @@ self.verify = async (req, res) => { } self.list = async (req, res) => { - const user = await utils.authorize(req) - return res.json({ success: true, token: user.token }) + return res.json({ success: true, token: req.locals.user.token }) } self.change = async (req, res) => { - const user = await utils.authorize(req, 'token') - const newToken = await self.getUniqueToken(res) await utils.db.table('users') - .where('token', user.token) + .where('token', req.locals.user.token) .update({ token: newToken, timestamp: Math.floor(Date.now() / 1000) diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 8afa3a4..bdb5a37 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -259,15 +259,8 @@ self.upload = async (req, res) => { throw new ClientError('Request Content-Type must be either multipart/form-data or application/json.') } - let user - if (config.private === true) { - user = await utils.authorize(req) - } else if (req.headers.token) { - user = await utils.assertUser(req.headers.token) - } - if (config.privateUploadGroup) { - if (!user || !perms.is(user, config.privateUploadGroup)) { + if (!req.locals.user || !perms.is(req.locals.user, config.privateUploadGroup)) { throw new ClientError(config.privateUploadCustomResponse || 'Your usergroup is not permitted to upload new files.', { statusCode: 403 }) } } @@ -275,18 +268,18 @@ self.upload = async (req, res) => { let albumid = parseInt(req.headers.albumid || (req.path_parameters && req.path_parameters.albumid)) if (isNaN(albumid)) albumid = null - const age = self.assertRetentionPeriod(user, req.headers.age) + const age = self.assertRetentionPeriod(req.locals.user, req.headers.age) if (isMultipart) { - return self.actuallyUpload(req, res, user, { albumid, age }) + return self.actuallyUpload(req, res, { albumid, age }) } else { // Parse POST body req.body = await req.json() - return self.actuallyUploadUrls(req, res, user, { albumid, age }) + return self.actuallyUploadUrls(req, res, { albumid, age }) } } -self.actuallyUpload = async (req, res, user, data = {}) => { +self.actuallyUpload = async (req, res, data = {}) => { // Init empty Request.body and Request.files req.body = {} req.files = [] @@ -471,7 +464,7 @@ self.actuallyUpload = async (req, res, user, data = {}) => { const filesData = req.files if (utils.scan.instance) { - const scanResult = await self.scanFiles(req, user, filesData) + const scanResult = await self.scanFiles(req, filesData) if (scanResult) { throw new ClientError(scanResult) } @@ -479,13 +472,13 @@ self.actuallyUpload = async (req, res, user, data = {}) => { await self.stripTags(req, filesData) - const result = await self.storeFilesToDb(req, res, user, filesData) - return self.sendUploadResponse(req, res, user, result) + const result = await self.storeFilesToDb(req, res, filesData) + return self.sendUploadResponse(req, res, result) } /** URL uploads */ -self.actuallyUploadUrls = async (req, res, user, data = {}) => { +self.actuallyUploadUrls = async (req, res, data = {}) => { if (!config.uploads.urlMaxSize) { throw new ClientError('Upload by URLs is disabled at the moment.', { statusCode: 403 }) } @@ -670,36 +663,23 @@ self.actuallyUploadUrls = async (req, res, user, data = {}) => { }) if (utils.scan.instance) { - const scanResult = await self.scanFiles(req, user, filesData) + const scanResult = await self.scanFiles(req, filesData) if (scanResult) { throw new ClientError(scanResult) } } - const result = await self.storeFilesToDb(req, res, user, filesData) - return self.sendUploadResponse(req, res, user, result) + const result = await self.storeFilesToDb(req, res, filesData) + return self.sendUploadResponse(req, res, result) } /** Chunk uploads */ self.finishChunks = async (req, res) => { - utils.assertRequestType(req, 'application/json') - if (!chunkedUploads) { throw new ClientError('Chunked upload is disabled.', { statusCode: 403 }) } - let user - if (config.private === true) { - user = await utils.authorize(req) - if (!user) return - } else if (req.headers.token) { - user = await utils.assertUser(req.headers.token) - } - - // Parse POST body - req.body = await req.json() - const files = req.body.files if (!Array.isArray(files) || !files.length) { throw new ClientError('Bad request.') @@ -710,7 +690,7 @@ self.finishChunks = async (req, res) => { file.uuid = `${req.ip}_${file.uuid}` }) - return self.actuallyFinishChunks(req, res, user, files) + return self.actuallyFinishChunks(req, res, files) .catch(error => { // Unlink temp files (do not wait) Promise.all(files.map(async file => { @@ -723,7 +703,7 @@ self.finishChunks = async (req, res) => { }) } -self.actuallyFinishChunks = async (req, res, user, files) => { +self.actuallyFinishChunks = async (req, res, files) => { const filesData = [] await Promise.all(files.map(async file => { if (!file.uuid || typeof chunksData[file.uuid] === 'undefined') { @@ -754,7 +734,7 @@ self.actuallyFinishChunks = async (req, res, user, files) => { throw new ClientError(`${extname ? `${extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`) } - const age = self.assertRetentionPeriod(user, file.age) + const age = self.assertRetentionPeriod(req.locals.user, file.age) let size = file.size if (size === undefined) { @@ -814,7 +794,7 @@ self.actuallyFinishChunks = async (req, res, user, files) => { })) if (utils.scan.instance) { - const scanResult = await self.scanFiles(req, user, filesData) + const scanResult = await self.scanFiles(req, filesData) if (scanResult) { throw new ClientError(scanResult) } @@ -822,8 +802,8 @@ self.actuallyFinishChunks = async (req, res, user, files) => { await self.stripTags(req, filesData) - const result = await self.storeFilesToDb(req, res, user, filesData) - return self.sendUploadResponse(req, res, user, result) + const result = await self.storeFilesToDb(req, res, filesData) + return self.sendUploadResponse(req, res, result) } self.cleanUpChunks = async uuid => { @@ -883,9 +863,9 @@ self.assertScanFileBypass = data => { return false } -self.scanFiles = async (req, user, filesData) => { +self.scanFiles = async (req, filesData) => { const filenames = filesData.map(file => file.filename) - if (self.assertScanUserBypass(user, filenames)) { + if (self.assertScanUserBypass(req.locals.user, filenames)) { return false } @@ -950,7 +930,7 @@ self.stripTags = async (req, filesData) => { /** Database functions */ -self.storeFilesToDb = async (req, res, user, filesData) => { +self.storeFilesToDb = async (req, res, filesData) => { const files = [] const exists = [] const albumids = [] @@ -960,10 +940,10 @@ self.storeFilesToDb = async (req, res, user, filesData) => { // Check if the file exists by checking its hash and size const dbFile = await utils.db.table('files') .where(function () { - if (user === undefined) { - this.whereNull('userid') + if (req.locals.user) { + this.where('userid', req.locals.user.id) } else { - this.where('userid', user.id) + this.whereNull('userid') } }) .where({ @@ -1002,8 +982,8 @@ self.storeFilesToDb = async (req, res, user, filesData) => { timestamp } - if (user) { - data.userid = user.id + if (req.locals.user) { + data.userid = req.locals.user.id data.albumid = file.albumid if (data.albumid !== null && !albumids.includes(data.albumid)) { albumids.push(data.albumid) @@ -1023,10 +1003,11 @@ self.storeFilesToDb = async (req, res, user, filesData) => { })) if (files.length) { + // albumids should be empty if non-registerd users (no auth requests) let authorizedIds = [] if (albumids.length) { authorizedIds = await utils.db.table('albums') - .where({ userid: user.id }) + .where({ userid: req.locals.user.id }) .whereIn('id', albumids) .select('id') .then(rows => rows.map(row => row.id)) @@ -1057,7 +1038,7 @@ self.storeFilesToDb = async (req, res, user, filesData) => { /** Final response */ -self.sendUploadResponse = async (req, res, user, result) => { +self.sendUploadResponse = async (req, res, result) => { // Send response return res.json({ success: true, @@ -1079,7 +1060,7 @@ self.sendUploadResponse = async (req, res, user, result) => { // If uploaded by user, add delete URL (intended for ShareX and its derivatives) // Homepage uploader will not use this (use dashboard instead) - if (user) { + if (req.locals.user) { map.deleteUrl = `${utils.conf.homeDomain}/file/${file.name}?delete` } @@ -1091,30 +1072,20 @@ self.sendUploadResponse = async (req, res, user, result) => { /** Delete uploads */ self.delete = async (req, res) => { - utils.assertRequestType(req, 'application/json') - - // Parse POST body and re-map for .bulkDelete() - // Original API used by lolisafe v3's frontend + // Re-map Request.body for .bulkDelete() + // This is the legacy API used by lolisafe v3's frontend // Meanwhile this fork's frontend uses .bulkDelete() straight away - req.body = await req.json() - .then(obj => { - const id = parseInt(obj.id) - return { - field: 'id', - values: isNaN(id) ? undefined : [id] - } - }) + const id = parseInt(req.body.id) + req.body = { + _legacy: true, + field: 'id', + values: isNaN(id) ? undefined : [id] + } return self.bulkDelete(req, res) } self.bulkDelete = async (req, res) => { - utils.assertRequestType(req, 'application/json') - const user = await utils.authorize(req) - - // Parse POST body, if required - req.body = req.body || await req.json() - const field = req.body.field || 'id' const values = req.body.values @@ -1122,7 +1093,7 @@ self.bulkDelete = async (req, res) => { throw new ClientError('No array of files specified.') } - const failed = await utils.bulkDeleteFromDb(field, values, user) + const failed = await utils.bulkDeleteFromDb(field, values, req.locals.user) return res.json({ success: true, failed }) } @@ -1130,13 +1101,13 @@ self.bulkDelete = async (req, res) => { /** List uploads */ self.list = async (req, res) => { - const user = await utils.authorize(req) - const all = req.headers.all === '1' const filters = req.headers.filters const minoffset = Number(req.headers.minoffset) || 0 - const ismoderator = perms.is(user, 'moderator') - if (all && !ismoderator) return res.status(403).end() + const ismoderator = perms.is(req.locals.user, 'moderator') + if (all && !ismoderator) { + return res.status(403).end() + } const albumid = req.path_parameters && Number(req.path_parameters.albumid) const basedomain = utils.conf.domain @@ -1522,7 +1493,7 @@ self.list = async (req, res) => { }) } else { // If not listing all uploads, list user's uploads - this.where('userid', user.id) + this.where('userid', req.locals.user.id) } // Then, refine using any of the supplied 'albumid' keys and/or NULL flag @@ -1746,8 +1717,7 @@ self.list = async (req, res) => { /** Get file info */ self.get = async (req, res) => { - const user = await utils.authorize(req) - const ismoderator = perms.is(user, 'moderator') + const ismoderator = perms.is(req.locals.user, 'moderator') const identifier = req.path_parameters && req.path_parameters.identifier if (identifier === undefined) { @@ -1757,8 +1727,9 @@ self.get = async (req, res) => { const file = await utils.db.table('files') .where('name', identifier) .where(function () { + // Only allow moderators to get any files' information if (!ismoderator) { - this.where('userid', user.id) + this.where('userid', req.locals.user.id) } }) .first() diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 1864230..f8230bf 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -398,33 +398,11 @@ self.assertRequestType = (req, type) => { } } -self.assertUser = async (token, fields) => { - const _fields = ['id', 'username', 'enabled', 'timestamp', 'permission', 'registration'] - if (typeof fields === 'string') fields = [fields] - if (Array.isArray(fields)) { - _fields.push(...fields) - } - - const user = await self.db.table('users') - .where('token', token) - .select(_fields) - .first() - if (user) { - if (user.enabled === false || user.enabled === 0) { - throw new ClientError('This account has been disabled.', { statusCode: 403 }) - } - return user - } else { - throw new ClientError('Invalid token.', { statusCode: 403 }) - } -} - -self.authorize = async (req, fields) => { - const token = req.headers.token - if (token === undefined) { - throw new ClientError('No token provided.', { statusCode: 403 }) - } - return self.assertUser(token, fields) +self.assertJSON = async (req, res) => { + // Assert Request Content-Type + self.assertRequestType(req, 'application/json') + // Parse JSON payload + req.body = await req.json() } self.generateThumbs = async (name, extname, force) => { @@ -801,10 +779,10 @@ self.invalidateStatsCache = type => { } const generateStats = async (req, res) => { - const user = await self.authorize(req) - - const isadmin = perms.is(user, 'admin') - if (!isadmin) throw new ClientError('', { statusCode: 403 }) + const isadmin = perms.is(req.locals.user, 'admin') + if (!isadmin) { + return res.status(403).end() + } const hrstart = process.hrtime() const stats = {} diff --git a/routes/api.js b/routes/api.js index c86e61e..87d5b92 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1,10 +1,10 @@ const { Router } = require('hyper-express') const routes = new Router() -const albumsController = require('./../controllers/albumsController') -const authController = require('./../controllers/authController') -const tokenController = require('./../controllers/tokenController') -const uploadController = require('./../controllers/uploadController') -const utilsController = require('./../controllers/utilsController') +const albums = require('./../controllers/albumsController') +const auth = require('./../controllers/authController') +const tokens = require('./../controllers/tokenController') +const upload = require('./../controllers/uploadController') +const utils = require('./../controllers/utilsController') const config = require('./../config') routes.get('/check', async (req, res) => { @@ -16,54 +16,75 @@ routes.get('/check', async (req, res) => { fileIdentifierLength: config.uploads.fileIdentifierLength, stripTags: config.uploads.stripTags } - if (utilsController.retentions.enabled && utilsController.retentions.periods._) { - obj.temporaryUploadAges = utilsController.retentions.periods._ - obj.defaultTemporaryUploadAge = utilsController.retentions.default._ + if (utils.retentions.enabled && utils.retentions.periods._) { + obj.temporaryUploadAges = utils.retentions.periods._ + obj.defaultTemporaryUploadAge = utils.retentions.default._ } - if (utilsController.clientVersion) { - obj.version = utilsController.clientVersion + if (utils.clientVersion) { + obj.version = utils.clientVersion } return res.json(obj) }) -routes.post('/login', authController.verify) -routes.post('/register', authController.register) -routes.post('/password/change', authController.changePassword) -routes.get('/uploads', uploadController.list) -routes.get('/uploads/:page', uploadController.list) -routes.post('/upload', uploadController.upload, { - // HyperExpress defaults to 250kb - // https://github.com/kartikk221/hyper-express/blob/6.2.4/docs/Server.md#server-constructor-options - max_body_length: parseInt(config.uploads.maxSize) * 1e6 -}) -routes.post('/upload/delete', uploadController.delete) -routes.post('/upload/bulkdelete', uploadController.bulkDelete) -routes.post('/upload/finishchunks', uploadController.finishChunks) -routes.get('/upload/get/:identifier', uploadController.get) -routes.post('/upload/:albumid', uploadController.upload) -routes.get('/album/get/:identifier', albumsController.get) -routes.get('/album/zip/:identifier', albumsController.generateZip) -routes.get('/album/:identifier', albumsController.getUpstreamCompat) -routes.get('/album/:albumid/:page', uploadController.list) -routes.get('/albums', albumsController.list) -routes.get('/albums/:page', albumsController.list) -routes.post('/albums', albumsController.create) -routes.post('/albums/addfiles', albumsController.addFiles) -routes.post('/albums/delete', albumsController.delete) -routes.post('/albums/disable', albumsController.disable) -routes.post('/albums/edit', albumsController.edit) -routes.post('/albums/rename', albumsController.rename) -routes.get('/albums/test', albumsController.test) -routes.get('/tokens', tokenController.list) -routes.post('/tokens/verify', tokenController.verify) -routes.post('/tokens/change', tokenController.change) -routes.get('/users', authController.listUsers) -routes.get('/users/:page', authController.listUsers) -routes.post('/users/create', authController.createUser) -routes.post('/users/edit', authController.editUser) -routes.post('/users/disable', authController.disableUser) -routes.post('/users/delete', authController.deleteUser) -routes.get('/stats', utilsController.stats) +/** ./controllers/authController.js */ + +routes.post('/login', utils.assertJSON, auth.verify) +routes.post('/register', utils.assertJSON, auth.register) +routes.post('/password/change', [auth.requireUser, utils.assertJSON], auth.changePassword) + +routes.get('/users', auth.requireUser, auth.listUsers) +routes.get('/users/:page', auth.requireUser, auth.listUsers) + +routes.post('/users/create', [auth.requireUser, utils.assertJSON], auth.createUser) +routes.post('/users/delete', [auth.requireUser, utils.assertJSON], auth.deleteUser) +routes.post('/users/disable', [auth.requireUser, utils.assertJSON], auth.disableUser) +routes.post('/users/edit', [auth.requireUser, utils.assertJSON], auth.editUser) + +/** ./controllers/uploadController.js */ + +// HyperExpress defaults to 250kb +// https://github.com/kartikk221/hyper-express/blob/6.4.4/docs/Server.md#server-constructor-options +const maxBodyLength = parseInt(config.uploads.maxSize) * 1e6 +routes.post('/upload', { max_body_length: maxBodyLength }, auth.optionalUser, upload.upload) +routes.post('/upload/:albumid', { max_body_length: maxBodyLength }, auth.optionalUser, upload.upload) +routes.post('/upload/finishchunks', [auth.optionalUser, utils.assertJSON], upload.finishChunks) + +routes.get('/uploads', auth.requireUser, upload.list) +routes.get('/uploads/:page', auth.requireUser, upload.list) +routes.get('/album/:albumid/:page', auth.requireUser, upload.list) + +routes.get('/upload/get/:identifier', auth.requireUser, upload.get) +routes.post('/upload/delete', [auth.requireUser, utils.assertJSON], upload.delete) +routes.post('/upload/bulkdelete', [auth.requireUser, utils.assertJSON], upload.bulkDelete) + +/** ./controllers/albumsController.js */ + +routes.get('/albums', auth.requireUser, albums.list) +routes.get('/albums/:page', auth.requireUser, albums.list) + +routes.get('/album/get/:identifier', albums.get) +routes.get('/album/zip/:identifier', albums.generateZip) +routes.get('/album/:identifier', albums.getUpstreamCompat) + +routes.post('/albums', [auth.requireUser, utils.assertJSON], albums.create) +routes.post('/albums/addfiles', [auth.requireUser, utils.assertJSON], albums.addFiles) +routes.post('/albums/delete', [auth.requireUser, utils.assertJSON], albums.delete) +routes.post('/albums/disable', [auth.requireUser, utils.assertJSON], albums.disable) +routes.post('/albums/edit', [auth.requireUser, utils.assertJSON], albums.edit) +routes.post('/albums/rename', [auth.requireUser, utils.assertJSON], albums.rename) + +/** ./controllers/tokenController.js **/ + +routes.get('/tokens', auth.requireUser, tokens.list) +routes.post('/tokens/change', async (req, res) => { + // Include user's "token" field into database query + return auth.requireUser(req, res, null, 'token') +}, tokens.change) +routes.post('/tokens/verify', utils.assertJSON, tokens.verify) + +/** ./controllers/utilsController.js */ + +routes.get('/stats', [auth.requireUser], utils.stats) module.exports = routes diff --git a/routes/nojs.js b/routes/nojs.js index a35a291..db34c0d 100644 --- a/routes/nojs.js +++ b/routes/nojs.js @@ -1,6 +1,6 @@ const { Router } = require('hyper-express') const routes = new Router() -const uploadController = require('./../controllers/uploadController') +const upload = require('./../controllers/uploadController') const utils = require('./../controllers/utilsController') const config = require('./../config') @@ -24,7 +24,7 @@ routes.post('/nojs', async (req, res) => { files: result.files || [{}] }) } - return uploadController.upload(req, res) + return upload.upload(req, res) }, { // HyperExpress defaults to 250kb // https://github.com/kartikk221/hyper-express/blob/6.2.4/docs/Server.md#server-constructor-options