diff --git a/.eslintrc.json b/.eslintrc.json index c7b8710..77fca09 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,7 +12,8 @@ "rules": { "curly": [ "error", - "multi" + "multi", + "consistent" ], "prefer-const": [ "error", diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 0df30ea..b9ce52c 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -143,8 +143,9 @@ uploadsController.upload = async (req, res, next) => { if (config.private === true) { user = await utils.authorize(req, res) if (!user) return - } else if (req.headers.token) + } else if (req.headers.token) { user = await db.table('users').where('token', req.headers.token).first() + } if (user && (user.enabled === false || user.enabled === 0)) return res.json({ success: false, description: 'This account has been disabled.' }) @@ -289,7 +290,9 @@ uploadsController.actuallyUploadByUrl = async (req, res, user, albumid) => { uploadsController.processFilesForDisplay(req, res, result.files, result.existingFiles) } }) - } catch (error) { erred(error) } + } catch (error) { + erred(error) + } } } @@ -301,8 +304,9 @@ uploadsController.finishChunks = async (req, res, next) => { if (config.private === true) { user = await utils.authorize(req, res) if (!user) return - } else if (req.headers.token) + } else if (req.headers.token) { user = await db.table('users').where('token', req.headers.token).first() + } if (user && (user.enabled === false || user.enabled === 0)) return res.json({ success: false, description: 'This account has been disabled.' }) @@ -624,10 +628,11 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles } uploadsController.delete = async (req, res) => { - const id = parseInt(req.body.id) + const id = parseInt(req.body.id) || parseInt(req.params.identifier) req.body.field = 'id' req.body.values = isNaN(id) ? undefined : [id] - if (req.body.id !== undefined) delete req.body.id + delete req.body.id + delete req.params.identifier return uploadsController.bulkDelete(req, res) } diff --git a/database/db.js b/database/db.js index ef24fc2..3e94fe8 100644 --- a/database/db.js +++ b/database/db.js @@ -3,69 +3,64 @@ const perms = require('./../controllers/permissionController') const init = function (db) { // Create the tables we need to store galleries and files db.schema.hasTable('albums').then(exists => { - if (!exists) { - db.schema.createTable('albums', function (table) { - table.increments() - table.integer('userid') - table.string('name') - table.string('identifier') - table.integer('enabled') - table.integer('timestamp') - table.integer('editedAt') - table.integer('zipGeneratedAt') - table.integer('download') - table.integer('public') - table.string('description') - }).then(() => {}) - } + if (exists) return + db.schema.createTable('albums', function (table) { + table.increments() + table.integer('userid') + table.string('name') + table.string('identifier') + table.integer('enabled') + table.integer('timestamp') + table.integer('editedAt') + table.integer('zipGeneratedAt') + table.integer('download') + table.integer('public') + table.string('description') + }).then(() => {}) }) db.schema.hasTable('files').then(exists => { - if (!exists) { - db.schema.createTable('files', function (table) { - table.increments() - table.integer('userid') - table.string('name') - table.string('original') - table.string('type') - table.string('size') - table.string('hash') - table.string('ip') - table.integer('albumid') - table.integer('timestamp') - }).then(() => {}) - } + if (exists) return + db.schema.createTable('files', function (table) { + table.increments() + table.integer('userid') + table.string('name') + table.string('original') + table.string('type') + table.string('size') + table.string('hash') + table.string('ip') + table.integer('albumid') + table.integer('timestamp') + }).then(() => {}) }) db.schema.hasTable('users').then(exists => { - if (!exists) { - db.schema.createTable('users', function (table) { - table.increments() - table.string('username') - table.string('password') - table.string('token') - table.integer('enabled') - table.integer('timestamp') - table.integer('fileLength') - table.integer('permission') - }).then(() => { - db.table('users').where({ username: 'root' }).then((user) => { - if (user.length > 0) { return } - - require('bcrypt').hash('root', 10, function (error, hash) { - if (error) { console.error('Error generating password hash for root') } - - db.table('users').insert({ - username: 'root', - password: hash, - token: require('randomstring').generate(64), - timestamp: Math.floor(Date.now() / 1000), - permission: perms.permissions.superadmin - }).then(() => {}) - }) + if (exists) return + db.schema.createTable('users', function (table) { + table.increments() + table.string('username') + table.string('password') + table.string('token') + table.integer('enabled') + table.integer('timestamp') + table.integer('fileLength') + table.integer('permission') + }).then(() => { + db.table('users').where({ username: 'root' }).then((user) => { + if (user.length > 0) return + require('bcrypt').hash('root', 10, function (error, hash) { + if (error) console.error('Error generating password hash for root') + db.table('users').insert({ + username: 'root', + password: hash, + token: require('randomstring').generate(64), + timestamp: Math.floor(Date.now() / 1000), + permission: perms.permissions.superadmin + }).then(() => {}) }) }) - } + }) }) } diff --git a/database/migration.js b/database/migration.js index 3ed13d0..1e03b3c 100644 --- a/database/migration.js +++ b/database/migration.js @@ -23,9 +23,9 @@ migration.start = async () => { await Promise.all(tables.map(table => { const columns = Object.keys(map[table]) return Promise.all(columns.map(async column => { - if (await db.schema.hasColumn(table, column)) { + if (await db.schema.hasColumn(table, column)) return // console.log(`SKIP: ${column} => ${table}.`) - } + const columnType = map[table][column] return db.schema.table(table, t => { t[columnType](column) }) .then(() => console.log(`OK: ${column} (${columnType}) => ${table}.`)) @@ -42,7 +42,7 @@ migration.start = async () => { .then(rows => { // NOTE: permissionController.js actually have a hard-coded check for "root" account so that // it will always have "superadmin" permission regardless of its permission value in database - if (!rows) { return console.log('Unable to update root\'s permission into superadmin.') } + if (!rows) return console.log('Unable to update root\'s permission into superadmin.') console.log(`Updated root's permission to ${perms.permissions.superadmin} (superadmin).`) }) diff --git a/lolisafe.js b/lolisafe.js index dfd590d..a6a930e 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -56,16 +56,15 @@ const setHeaders = res => { res.set('Cache-Control', 'public, max-age=2592000, must-revalidate, proxy-revalidate, immutable, stale-while-revalidate=86400, stale-if-error=604800') // max-age: 30 days } -if (config.serveFilesWithNode) { +if (config.serveFilesWithNode) safe.use('/', express.static(config.uploads.folder, { setHeaders })) -} safe.use('/', express.static('./public', { setHeaders })) safe.use('/', album) safe.use('/', nojs) safe.use('/api', api) -for (const page of config.pages) { +for (const page of config.pages) if (fs.existsSync(`./pages/custom/${page}.html`)) { safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { root: './pages/custom/' @@ -90,7 +89,6 @@ for (const page of config.pages) { } else { safe.get(`/${page}`, (req, res, next) => res.render(page)) } -} safe.use((req, res, next) => { res.status(404).sendFile(config.errorPages[404], { root: config.errorPages.rootDir }) @@ -109,31 +107,31 @@ const start = async () => { if (config.showGitHash) { const gitHash = await new Promise((resolve, reject) => { require('child_process').exec('git rev-parse HEAD', (error, stdout) => { - if (error) { return reject(error) } + if (error) return reject(error) resolve(stdout.replace(/\n$/, '')) }) }).catch(console.error) - if (!gitHash) { return } + if (!gitHash) return console.log(`Git commit: ${gitHash}`) safe.set('git-hash', gitHash) } if (config.uploads.scan && config.uploads.scan.enabled) { const created = await new Promise(async (resolve, reject) => { - if (!config.uploads.scan.ip || !config.uploads.scan.port) { + if (!config.uploads.scan.ip || !config.uploads.scan.port) return reject(new Error('clamd IP or port is missing')) - } + const ping = await clamd.ping(config.uploads.scan.ip, config.uploads.scan.port).catch(reject) - if (!ping) { + if (!ping) return reject(new Error('Could not ping clamd')) - } + const version = await clamd.version(config.uploads.scan.ip, config.uploads.scan.port).catch(reject) console.log(`${config.uploads.scan.ip}:${config.uploads.scan.port} ${version}`) const scanner = clamd.createScanner(config.uploads.scan.ip, config.uploads.scan.port) safe.set('clam-scanner', scanner) return resolve(true) }).catch(error => console.error(error.toString())) - if (!created) { return process.exit(1) } + if (!created) return process.exit(1) } if (config.uploads.cacheFileIdentifiers) { @@ -142,40 +140,39 @@ const start = async () => { const setSize = await new Promise((resolve, reject) => { const uploadsDir = `./${config.uploads.folder}` fs.readdir(uploadsDir, (error, names) => { - if (error) { return reject(error) } + if (error) return reject(error) const set = new Set() names.forEach(name => set.add(name.split('.')[0])) safe.set('uploads-set', set) resolve(set.size) }) }).catch(error => console.error(error.toString())) - if (!setSize) { return process.exit(1) } + if (!setSize) return process.exit(1) process.stdout.write(` ${setSize} OK!\n`) } safe.listen(config.port, () => { console.log(`lolisafe started on port ${config.port}`) + // DEV=1 yarn start if (process.env.DEV === '1') { - // DEV=1 yarn start - console.log('lolisafe is in development mode, nunjucks caching disabled') + // Add readline interface to allow evaluating arbitrary JavaScript from console + readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '' + }).on('line', line => { + try { + if (line === '.exit') process.exit(0) + // eslint-disable-next-line no-eval + process.stdout.write(`${require('util').inspect(eval(line), { depth: 0 })}\n`) + } catch (error) { + console.error(error.toString()) + } + }).on('SIGINT', () => { + process.exit(0) + }) + console.warn('development mode enabled (disabled nunjucks caching & enabled readline interface)') } - - // Add readline interface to allow evaluating arbitrary JavaScript from console - readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: '' - }).on('line', line => { - try { - if (line === '.exit') { process.exit(0) } - // eslint-disable-next-line no-eval - process.stdout.write(`${require('util').inspect(eval(line), { depth: 0 })}\n`) - } catch (error) { - console.error(error.toString()) - } - }).on('SIGINT', () => { - process.exit(0) - }) }) } diff --git a/public/js/.eslintrc.json b/public/js/.eslintrc.json index 9c699d4..0780170 100644 --- a/public/js/.eslintrc.json +++ b/public/js/.eslintrc.json @@ -12,7 +12,8 @@ "rules": { "curly": [ "error", - "multi" + "multi", + "consistent" ], "quotes": [ "error", diff --git a/public/js/dashboard.js b/public/js/dashboard.js index e5d16f5..5e30b56 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -209,8 +209,9 @@ page.domClick = function (event) { views.album = page.views.uploads.album views.all = page.views.uploads.all func = page.getUploads - } else if (page.currentView === 'users') + } else if (page.currentView === 'users') { func = page.getUsers + } switch (action) { case 'page-prev': @@ -785,8 +786,9 @@ page.deleteSelectedFiles = function () { page.selected.uploads = page.selected.uploads.filter(function (id) { return bulkdelete.data.failed.includes(id) }) - } else + } else { page.selected.uploads = [] + } localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected.uploads) @@ -1801,10 +1803,11 @@ page.editUser = function (id) { icon: 'success', content: div }) - } else if (response.data.name !== user.name) + } else if (response.data.name !== user.name) { swal('Success!', `${user.username} was renamed into: ${response.data.name}.`, 'success') - else + } else { swal('Success!', 'The user was edited!', 'success') + } page.getUsers(page.views.users) }).catch(function (error) { @@ -1829,17 +1832,16 @@ page.disableUser = function (id) { } } }).then(function (proceed) { - if (!proceed) { return } + if (!proceed) return axios.post('api/users/disable', { id }).then(function (response) { - if (!response) { return } + if (!response) return if (response.data.success === false) { - if (response.data.description === 'No token provided') { + if (response.data.description === 'No token provided') return page.verifyToken(page.token) - } else { + else return swal('An error occurred!', response.data.description, 'error') - } } swal('Success!', 'The user has been disabled.', 'success') diff --git a/public/js/home.js b/public/js/home.js index d02dfeb..d8d3be1 100644 --- a/public/js/home.js +++ b/public/js/home.js @@ -121,8 +121,9 @@ page.prepareUpload = function () { page.uploadUrls(this) }) page.setActiveTab('tab-files') - } else + } else { document.getElementById('tab-files').style.display = 'block' + } } page.prepareAlbums = function () { @@ -296,7 +297,9 @@ page.uploadUrls = function (button) { const previewsContainer = tabDiv.getElementsByClassName('uploads')[0] const urls = document.getElementById('urls').value .split(/\r?\n/) - .filter(function (url) { return url.trim().length }) + .filter(function (url) { + return url.trim().length + }) document.getElementById('urls').value = urls.join('\n') if (!urls.length) @@ -365,7 +368,11 @@ page.updateTemplate = function (file, response) { const img = file.previewElement.querySelector('img') img.setAttribute('alt', response.name || '') img.dataset['src'] = response.url - img.onerror = function () { this.style.display = 'none' } // hide webp in firefox and ie + img.onerror = function () { + // Hide images that failed to load + // Consequently also WEBP in browsers that do not have WEBP support (Firefox/IE) + this.style.display = 'none' + } page.lazyLoad.update(file.previewElement.querySelectorAll('img')) } } diff --git a/routes/album.js b/routes/album.js index 045b79a..22ba625 100644 --- a/routes/album.js +++ b/routes/album.js @@ -8,12 +8,11 @@ const homeDomain = config.homeDomain || config.domain routes.get('/a/:identifier', async (req, res, next) => { const identifier = req.params.identifier - if (identifier === undefined) { + if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided.' }) - } const album = await db.table('albums') .where({ @@ -22,14 +21,13 @@ routes.get('/a/:identifier', async (req, res, next) => { }) .first() - if (!album) { + if (!album) return res.status(404).sendFile('404.html', { root: './pages/error/' }) - } else if (album.public === 0) { + else if (album.public === 0) return res.status(401).json({ success: false, description: 'This album is not available for public.' }) - } const files = await db.table('files') .select('name', 'size') @@ -42,19 +40,17 @@ routes.get('/a/:identifier', async (req, res, next) => { let totalSize = 0 for (const file of files) { file.file = `${basedomain}/${file.name}` - totalSize += parseInt(file.size) - file.extname = path.extname(file.name).toLowerCase() if (utils.mayGenerateThumb(file.extname)) { file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png` - /* If thumbnail for album is still not set, do it. A potential improvement would be to let the user upload a specific image as an album cover since embedding the first image could potentially result in nsfw content when pasting links. */ - if (thumb === '') { thumb = file.thumb } + if (thumb === '') thumb = file.thumb } + totalSize += parseInt(file.size) } return res.render('album', { diff --git a/routes/api.js b/routes/api.js index 6124077..84c2817 100644 --- a/routes/api.js +++ b/routes/api.js @@ -21,6 +21,7 @@ routes.get('/uploads', (req, res, next) => uploadController.list(req, res, next) routes.get('/uploads/:page', (req, res, next) => uploadController.list(req, res, next)) routes.post('/upload', (req, res, next) => uploadController.upload(req, res, next)) routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next)) +routes.get('/upload/delete/:identifier', (req, res, next) => uploadController.delete(req, res, next)) routes.post('/upload/bulkdelete', (req, res, next) => uploadController.bulkDelete(req, res, next)) routes.post('/upload/finishchunks', (req, res, next) => uploadController.finishChunks(req, res, next)) routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next)) diff --git a/scripts/thumbs.js b/scripts/thumbs.js index 343086b..3be88f4 100644 --- a/scripts/thumbs.js +++ b/scripts/thumbs.js @@ -15,13 +15,13 @@ thumbs.mayGenerateThumb = extname => { thumbs.getFiles = directory => { return new Promise((resolve, reject) => { fs.readdir(directory, async (error, names) => { - if (error) { return reject(error) } + if (error) return reject(error) const files = [] await Promise.all(names.map(name => { return new Promise((resolve, reject) => { fs.lstat(path.join(directory, name), (error, stats) => { - if (error) { return reject(error) } - if (stats.isFile() && !name.startsWith('.')) { files.push(name) } + if (error) return reject(error) + if (stats.isFile() && !name.startsWith('.')) files.push(name) resolve() }) }) @@ -62,16 +62,16 @@ thumbs.do = async () => { await new Promise((resolve, reject) => { const generate = async i => { const _upload = _uploads[i] - if (!_upload) { return resolve() } + if (!_upload) return resolve() const extname = path.extname(_upload) const basename = _upload.slice(0, -extname.length) if (_thumbs.includes(basename) && !thumbs.force) { - if (thumbs.verbose) { console.log(`${_upload}: thumb exists.`) } + if (thumbs.verbose) console.log(`${_upload}: thumb exists.`) skipped++ } else if (!thumbs.mayGenerateThumb(extname)) { - if (thumbs.verbose) { console.log(`${_upload}: extension skipped.`) } + if (thumbs.verbose) console.log(`${_upload}: extension skipped.`) skipped++ } else { const generated = await utils.generateThumbs(_upload, thumbs.force)