From 616124446f3f6c740c222c6a08d84c5cd21071ec Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Sat, 24 Mar 2018 20:52:47 +0700 Subject: [PATCH] Updates (WARNING!) WARNING: Please turn off lolisafe before upgrading, then run "node database/migration.js" once after upgrading. Ignore all errors/warnings about duplicate column name. Afterwards make sure your config.js follows the new format in config.sample.js (specifically fileLength and generateThumbnails options). * generateImageThumbnails and generateVideoThumbnails options in config.js is now renamed to an object named generateThumbnails, with image and video as its properties. * fileLength option is now an object with min, max, default and userChangeable as its properties. * User may now change their preferred file length (following the previous option, of course). * Updated a bunch of responses messages. Mainly appending a dot to the messages. * New APIs: /fileLength/config to get an object of the current fileLength config (exactly what is in the config.js file). /fileLength/change to change user's preferred file length. * And maybe some others ...? --- config.sample.js | 23 ++++++++++--- controllers/albumsController.js | 26 +++++++------- controllers/authController.js | 60 +++++++++++++++++++++++--------- controllers/tokenController.js | 4 +-- controllers/uploadController.js | 29 +++++++++------- controllers/utilsController.js | 8 ++--- database/migration.js | 11 +++--- pages/album.html | 2 +- pages/auth.html | 2 +- pages/dashboard.html | 9 +++-- pages/faq.html | 2 +- pages/home.html | 2 +- public/css/style.css | 4 +++ public/js/dashboard.js | 61 +++++++++++++++++++++++++++++++-- routes/album.js | 2 +- routes/api.js | 2 ++ views/album.handlebars | 2 +- 17 files changed, 179 insertions(+), 70 deletions(-) diff --git a/config.sample.js b/config.sample.js index 912265a..d5c65d2 100644 --- a/config.sample.js +++ b/config.sample.js @@ -64,8 +64,21 @@ module.exports = { */ maxSize: '512MB', - // The length of the random generated name for the uploaded files - fileLength: 32, + /* + The length of the random generated name for the uploaded files. + If "userChangeable" is set to true, registered users will be able to change + their preferred file name length from the dashboard. The allowed range will + be set by "min" and "max". Otherwise it will use "default". + Technically it's possible to have "default" outside of the "min" and "max" range, + but please not. Once a user has changed to a number within the range, the user will + no longer be able to use the default value. + */ + fileLength: { + min: 4, + max: 32, + default: 32, + userChangeable: false + }, /* This option will limit how many times it will try to generate random names @@ -79,8 +92,10 @@ module.exports = { to install a separate binary called graphicsmagick (http://www.graphicsmagick.org) for images and ffmpeg (https://ffmpeg.org/) for video files */ - generateImageThumbnails: true, - generateVideoThumbnails: false, + generateThumbnails: { + image: true, + video: false + }, /* Allows users to download a .zip file of all files in an album. diff --git a/controllers/albumsController.js b/controllers/albumsController.js index a07449a..d5331e6 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -46,7 +46,7 @@ albumsController.create = async (req, res, next) => { const name = req.body.name if (name === undefined || name === '') { - return res.json({ success: false, description: 'No album name specified' }) + return res.json({ success: false, description: 'No album name specified.' }) } const album = await db.table('albums').where({ @@ -56,7 +56,7 @@ albumsController.create = async (req, res, next) => { }).first() if (album) { - return res.json({ success: false, description: 'There\'s already an album with that name' }) + return res.json({ success: false, description: 'There\'s already an album with that name.' }) } await db.table('albums').insert({ @@ -77,7 +77,7 @@ albumsController.delete = async (req, res, next) => { const id = req.body.id if (id === undefined || id === '') { - return res.json({ success: false, description: 'No album specified' }) + return res.json({ success: false, description: 'No album specified.' }) } await db.table('albums').where({ id: id, userid: user.id }).update({ enabled: 0 }) @@ -89,17 +89,17 @@ albumsController.rename = async (req, res, next) => { const id = req.body.id if (id === undefined || id === '') { - return res.json({ success: false, description: 'No album specified' }) + return res.json({ success: false, description: 'No album specified.' }) } const name = req.body.name if (name === undefined || name === '') { - return res.json({ success: false, description: 'No name specified' }) + return res.json({ success: false, description: 'No name specified.' }) } const album = await db.table('albums').where({ name: name, userid: user.id }).first() if (album) { - return res.json({ success: false, description: 'Name already in use' }) + return res.json({ success: false, description: 'Name already in use.' }) } await db.table('albums').where({ id: id, userid: user.id }).update({ name: name }) @@ -108,10 +108,10 @@ albumsController.rename = async (req, res, next) => { albumsController.get = async (req, res, next) => { const identifier = req.params.identifier - if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' }) + if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided.' }) const album = await db.table('albums').where({ identifier, enabled: 1 }).first() - if (!album) return res.json({ success: false, description: 'Album not found' }) + if (!album) return res.json({ success: false, description: 'Album not found.' }) const title = album.name const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC') @@ -120,7 +120,7 @@ albumsController.get = async (req, res, next) => { file.file = `${config.domain}/${file.name}` const ext = path.extname(file.name).toLowerCase() - if ((config.uploads.generateImageThumbnails && utils.imageExtensions.includes(ext)) || (config.uploads.generateVideoThumbnails && utils.videoExtensions.includes(ext))) { + if ((config.uploads.generateThumbnails.image && utils.imageExtensions.includes(ext)) || (config.uploads.generateThumbnails.video && utils.videoExtensions.includes(ext))) { file.thumb = `${config.domain}/thumbs/${file.name.slice(0, -ext.length)}.png` } } @@ -135,11 +135,11 @@ albumsController.get = async (req, res, next) => { albumsController.generateZip = async (req, res, next) => { 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' }) + 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').where({ identifier, enabled: 1 }).first() - if (!album) return res.json({ success: false, description: 'Album not found' }) + if (!album) return res.json({ success: false, description: 'Album not found.' }) if (album.zipGeneratedAt > album.editedAt) { const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`) @@ -148,7 +148,7 @@ albumsController.generateZip = async (req, res, next) => { } else { console.log(`Generating zip for album identifier: ${identifier}`) const files = await db.table('files').select('name').where('albumid', album.id) - if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album' }) + if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album.' }) const zipPath = path.join(__dirname, '..', config.uploads.folder, 'zips', `${album.identifier}.zip`) let archive = new Zip() diff --git a/controllers/authController.js b/controllers/authController.js index f54c5d3..c625c85 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -10,53 +10,53 @@ authController.verify = async (req, res, next) => { const username = req.body.username const password = req.body.password - if (username === undefined) return res.json({ success: false, description: 'No username provided' }) - if (password === undefined) return res.json({ success: false, description: 'No password provided' }) + if (username === undefined) return res.json({ success: false, description: 'No username provided.' }) + if (password === undefined) return res.json({ success: false, description: 'No password provided.' }) const user = await db.table('users').where('username', username).first() - if (!user) return res.json({ success: false, description: 'Username doesn\'t exist' }) + if (!user) return res.json({ success: false, description: 'Username doesn\'t exist.' }) if (user.enabled === false || user.enabled === 0) { return res.json({ success: false, - description: 'This account has been disabled' + description: 'This account has been disabled.' }) } bcrypt.compare(password, user.password, (err, result) => { if (err) { console.log(err) - return res.json({ success: false, description: 'There was an error' }) + return res.json({ success: false, description: 'There was an error.' }) } - if (result === false) return res.json({ success: false, description: 'Wrong password' }) + if (result === false) return res.json({ success: false, description: 'Wrong password.' }) return res.json({ success: true, token: user.token }) }) } authController.register = async (req, res, next) => { if (config.enableUserAccounts === false) { - return res.json({ success: false, description: 'Register is disabled at the moment' }) + return res.json({ success: false, description: 'Register is disabled at the moment.' }) } const username = req.body.username const password = req.body.password - if (username === undefined) return res.json({ success: false, description: 'No username provided' }) - if (password === undefined) return res.json({ success: false, description: 'No password provided' }) + if (username === undefined) return res.json({ success: false, description: 'No username provided.' }) + if (password === undefined) return res.json({ success: false, description: 'No password provided.' }) if (username.length < 4 || username.length > 32) { - return res.json({ success: false, description: 'Username must have 4-32 characters' }) + return res.json({ success: false, description: 'Username must have 4-32 characters.' }) } if (password.length < 6 || password.length > 64) { - return res.json({ success: false, description: 'Password must have 6-64 characters' }) + return res.json({ success: false, description: 'Password must have 6-64 characters.' }) } const user = await db.table('users').where('username', username).first() - if (user) return res.json({ success: false, description: 'Username already exists' }) + if (user) return res.json({ success: false, description: 'Username already exists.' }) bcrypt.hash(password, 10, async (err, hash) => { if (err) { console.log(err) - return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' }) + return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻.' }) } const token = randomstring.generate(64) await db.table('users').insert({ @@ -73,16 +73,16 @@ authController.changePassword = async (req, res, next) => { const user = await utils.authorize(req, res) let password = req.body.password - if (password === undefined) return res.json({ success: false, description: 'No password provided' }) + if (password === undefined) return res.json({ success: false, description: 'No password provided.' }) if (password.length < 6 || password.length > 64) { - return res.json({ success: false, description: 'Password must have 6-64 characters' }) + return res.json({ success: false, description: 'Password must have 6-64 characters.' }) } bcrypt.hash(password, 10, async (err, hash) => { if (err) { console.log(err) - return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' }) + return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻.' }) } await db.table('users').where('id', user.id).update({ password: hash }) @@ -90,4 +90,32 @@ authController.changePassword = async (req, res, next) => { }) } +authController.getFileLengthConfig = async (req, res, next) => { + const user = await utils.authorize(req, res) + return res.json({ success: true, fileLength: user.fileLength, config: config.uploads.fileLength }) +} + +authController.changeFileLength = async (req, res, next) => { + if (config.uploads.fileLength.userChangeable === false) { + return res.json({ success: false, description: 'Changing file length is disabled at the moment.' }) + } + + const user = await utils.authorize(req, res) + + let fileLength = parseInt(req.body.fileLength) + if (fileLength === undefined) return res.json({ success: false, description: 'No file length provided.' }) + if (isNaN(fileLength)) return res.json({ success: false, description: 'File length is not a valid number.' }) + + if (fileLength < config.uploads.fileLength.min || fileLength > config.uploads.fileLength.max) { + return res.json({ success: false, description: `File length must be ${config.uploads.fileLength.min} to ${config.uploads.fileLength.max} characters` }) + } + + if (fileLength === user.fileLength) { + return res.json({ success: true }) + } + + await db.table('users').where('id', user.id).update({ fileLength }) + return res.json({ success: true }) +} + module.exports = authController diff --git a/controllers/tokenController.js b/controllers/tokenController.js index bb8f9c9..15b1d1c 100644 --- a/controllers/tokenController.js +++ b/controllers/tokenController.js @@ -7,10 +7,10 @@ const tokenController = {} tokenController.verify = async (req, res, next) => { const token = req.body.token - if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' }) + if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided.' }) const user = await db.table('users').where('token', token).first() - if (!user) return res.status(401).json({ success: false, description: 'Invalid token' }) + if (!user) return res.status(401).json({ success: false, description: 'Invalid token.' }) return res.json({ success: true, username: user.username }) } diff --git a/controllers/uploadController.js b/controllers/uploadController.js index d924f4c..600a998 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -18,8 +18,10 @@ const storage = multer.diskStorage({ cb(null, uploadDir) }, filename: function (req, file, cb) { + // If the user has a preferred file length, make sure it follows the allowed range + const fileLength = req.params.fileLength ? Math.min(Math.max(req.params.fileLength, config.uploads.fileLength.min), config.uploads.fileLength.max) : config.uploads.fileLength.default const access = i => { - const name = randomstring.generate(config.uploads.fileLength) + path.extname(file.originalname) + const name = randomstring.generate(fileLength) + path.extname(file.originalname) fs.access(path.join(uploadDir, name), err => { if (err) return cb(null, name) console.log(`A file named "${name}" already exists (${++i}/${maxTries}).`) @@ -36,12 +38,10 @@ const upload = multer({ storage: storage, limits: { fileSize: config.uploads.maxSize }, fileFilter: function (req, file, cb) { - if (config.blockedExtensions !== undefined) { - if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) { - // eslint-disable-next-line standard/no-callback-literal - return cb('This file extension is not allowed') - } - return cb(null, true) + if (config.blockedExtensions === undefined) return cb(null, true) + if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) { + // eslint-disable-next-line standard/no-callback-literal + return cb('This file extension is not allowed.') } return cb(null, true) } @@ -57,9 +57,12 @@ uploadsController.upload = async (req, res, next) => { if (user && (user.enabled === false || user.enabled === 0)) { return res.json({ success: false, - description: 'This account has been disabled' + description: 'This account has been disabled.' }) } + if (user && user.fileLength) { + req.params.fileLength = user.fileLength + } const albumid = req.headers.albumid || req.params.albumid if (albumid && user) { @@ -67,7 +70,7 @@ uploadsController.upload = async (req, res, next) => { if (!album) { return res.json({ success: false, - description: 'Album doesn\'t exist or it doesn\'t belong to the user' + description: 'Album doesn\'t exist or it doesn\'t belong to the user.' }) } return uploadsController.actuallyUpload(req, res, user, albumid) @@ -167,14 +170,14 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles for (let file of files) { let ext = path.extname(file.name).toLowerCase() - if ((config.uploads.generateImageThumbnails && utils.imageExtensions.includes(ext)) || (config.uploads.generateVideoThumbnails && utils.videoExtensions.includes(ext))) { + if ((config.uploads.generateThumbnails.image && utils.imageExtensions.includes(ext)) || (config.uploads.generateThumbnails.video && utils.videoExtensions.includes(ext))) { file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png` utils.generateThumbs(file) } if (file.albumid) { db.table('albums').where('id', file.albumid).update('editedAt', file.timestamp).then(() => {}) - .catch(error => { console.log(error); res.json({ success: false, description: 'Error updating album' }) }) + .catch(error => { console.log(error); res.json({ success: false, description: 'Error updating album.' }) }) } } } @@ -183,7 +186,7 @@ uploadsController.delete = async (req, res) => { const user = await utils.authorize(req, res) const id = req.body.id if (id === undefined || id === '') { - return res.json({ success: false, description: 'No file specified' }) + return res.json({ success: false, description: 'No file specified.' }) } const file = await db.table('files') @@ -285,7 +288,7 @@ uploadsController.list = async (req, res) => { } let ext = path.extname(file.name).toLowerCase() - if ((config.uploads.generateImageThumbnails && utils.imageExtensions.includes(ext)) || (config.uploads.generateVideoThumbnails && utils.videoExtensions.includes(ext))) { + if ((config.uploads.generateThumbnails.image && utils.imageExtensions.includes(ext)) || (config.uploads.generateThumbnails.video && utils.videoExtensions.includes(ext))) { file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png` } } diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 101932f..54f08db 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -23,10 +23,10 @@ utilsController.getPrettyDate = function (date) { utilsController.authorize = async (req, res) => { const token = req.headers.token - if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' }) + if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided.' }) const user = await db.table('users').where('token', token).first() - if (!user) return res.status(401).json({ success: false, description: 'Invalid token' }) + if (!user) return res.status(401).json({ success: false, description: 'Invalid token.' }) return user } @@ -36,8 +36,8 @@ utilsController.generateThumbs = function (file, basedomain) { const isImageExt = utilsController.imageExtensions.includes(ext) if (!isVideoExt && !isImageExt) return - if (isVideoExt && config.uploads.generateVideoThumbnails !== true) return - if (isImageExt && config.uploads.generateImageThumbnails !== true) return + if (isVideoExt && config.uploads.generateThumbnails.video !== true) return + if (isImageExt && config.uploads.generateThumbnails.image !== true) return let thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png') fs.access(thumbname, err => { diff --git a/database/migration.js b/database/migration.js index 0e0162e..4241d75 100644 --- a/database/migration.js +++ b/database/migration.js @@ -3,13 +3,10 @@ const db = require('knex')(config.database) const migration = {} migration.start = async () => { - await db.schema.table('albums', table => { - table.dateTime('editedAt') - table.dateTime('zipGeneratedAt') - }).catch(() => {}) - await db.schema.table('users', table => { - table.integer('enabled') - }).catch(() => {}) + await db.schema.table('albums', t => t.dateTime('editedAt')).catch(err => console.warn(err.message)) + await db.schema.table('albums', t => t.dateTime('zipGeneratedAt')).catch(err => console.warn(err.message)) + await db.schema.table('users', t => t.dateTime('enabled')).catch(err => console.warn(err.message)) + await db.schema.table('users', t => t.dateTime('fileLength')).catch(err => console.warn(err.message)) console.log('Migration finished! Now start lolisafe normally') process.exit(0) } diff --git a/pages/album.html b/pages/album.html index 4e1e68e..795c942 100644 --- a/pages/album.html +++ b/pages/album.html @@ -11,7 +11,7 @@ - + diff --git a/pages/auth.html b/pages/auth.html index 7188b8c..5f4104c 100644 --- a/pages/auth.html +++ b/pages/auth.html @@ -12,7 +12,7 @@ - + diff --git a/pages/dashboard.html b/pages/dashboard.html index a24068d..a22c939 100644 --- a/pages/dashboard.html +++ b/pages/dashboard.html @@ -12,8 +12,8 @@ - - + + @@ -108,7 +108,10 @@