Updated ESLint rule: curly, again.
Mainly to also enabled "consistent" rule, which enforces curly into
else/elseif blocks, if its if block requires curly.

Added support for GET requests to /api/delete route.
Its usage is /api/delete/identifier, where identifier is the filename.
Though just like its POST route, it needs token in the header.
This commit is contained in:
Bobby Wibowo 2018-12-19 00:41:42 +07:00
parent 52d336cc45
commit 00cbd3e76c
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
11 changed files with 129 additions and 124 deletions

View File

@ -12,7 +12,8 @@
"rules": { "rules": {
"curly": [ "curly": [
"error", "error",
"multi" "multi",
"consistent"
], ],
"prefer-const": [ "prefer-const": [
"error", "error",

View File

@ -143,8 +143,9 @@ uploadsController.upload = async (req, res, next) => {
if (config.private === true) { if (config.private === true) {
user = await utils.authorize(req, res) user = await utils.authorize(req, res)
if (!user) return if (!user) return
} else if (req.headers.token) } else if (req.headers.token) {
user = await db.table('users').where('token', req.headers.token).first() user = await db.table('users').where('token', req.headers.token).first()
}
if (user && (user.enabled === false || user.enabled === 0)) if (user && (user.enabled === false || user.enabled === 0))
return res.json({ success: false, description: 'This account has been disabled.' }) 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) 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) { if (config.private === true) {
user = await utils.authorize(req, res) user = await utils.authorize(req, res)
if (!user) return if (!user) return
} else if (req.headers.token) } else if (req.headers.token) {
user = await db.table('users').where('token', req.headers.token).first() user = await db.table('users').where('token', req.headers.token).first()
}
if (user && (user.enabled === false || user.enabled === 0)) if (user && (user.enabled === false || user.enabled === 0))
return res.json({ success: false, description: 'This account has been disabled.' }) 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) => { 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.field = 'id'
req.body.values = isNaN(id) ? undefined : [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) return uploadsController.bulkDelete(req, res)
} }

View File

@ -3,7 +3,7 @@ const perms = require('./../controllers/permissionController')
const init = function (db) { const init = function (db) {
// Create the tables we need to store galleries and files // Create the tables we need to store galleries and files
db.schema.hasTable('albums').then(exists => { db.schema.hasTable('albums').then(exists => {
if (!exists) { if (exists) return
db.schema.createTable('albums', function (table) { db.schema.createTable('albums', function (table) {
table.increments() table.increments()
table.integer('userid') table.integer('userid')
@ -17,11 +17,10 @@ const init = function (db) {
table.integer('public') table.integer('public')
table.string('description') table.string('description')
}).then(() => {}) }).then(() => {})
}
}) })
db.schema.hasTable('files').then(exists => { db.schema.hasTable('files').then(exists => {
if (!exists) { if (exists) return
db.schema.createTable('files', function (table) { db.schema.createTable('files', function (table) {
table.increments() table.increments()
table.integer('userid') table.integer('userid')
@ -34,11 +33,10 @@ const init = function (db) {
table.integer('albumid') table.integer('albumid')
table.integer('timestamp') table.integer('timestamp')
}).then(() => {}) }).then(() => {})
}
}) })
db.schema.hasTable('users').then(exists => { db.schema.hasTable('users').then(exists => {
if (!exists) { if (exists) return
db.schema.createTable('users', function (table) { db.schema.createTable('users', function (table) {
table.increments() table.increments()
table.string('username') table.string('username')
@ -50,11 +48,9 @@ const init = function (db) {
table.integer('permission') table.integer('permission')
}).then(() => { }).then(() => {
db.table('users').where({ username: 'root' }).then((user) => { db.table('users').where({ username: 'root' }).then((user) => {
if (user.length > 0) { return } if (user.length > 0) return
require('bcrypt').hash('root', 10, function (error, hash) { require('bcrypt').hash('root', 10, function (error, hash) {
if (error) { console.error('Error generating password hash for root') } if (error) console.error('Error generating password hash for root')
db.table('users').insert({ db.table('users').insert({
username: 'root', username: 'root',
password: hash, password: hash,
@ -65,7 +61,6 @@ const init = function (db) {
}) })
}) })
}) })
}
}) })
} }

View File

@ -23,9 +23,9 @@ migration.start = async () => {
await Promise.all(tables.map(table => { await Promise.all(tables.map(table => {
const columns = Object.keys(map[table]) const columns = Object.keys(map[table])
return Promise.all(columns.map(async column => { 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}.`) return // console.log(`SKIP: ${column} => ${table}.`)
}
const columnType = map[table][column] const columnType = map[table][column]
return db.schema.table(table, t => { t[columnType](column) }) return db.schema.table(table, t => { t[columnType](column) })
.then(() => console.log(`OK: ${column} (${columnType}) => ${table}.`)) .then(() => console.log(`OK: ${column} (${columnType}) => ${table}.`))
@ -42,7 +42,7 @@ migration.start = async () => {
.then(rows => { .then(rows => {
// NOTE: permissionController.js actually have a hard-coded check for "root" account so that // 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 // 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).`) console.log(`Updated root's permission to ${perms.permissions.superadmin} (superadmin).`)
}) })

View File

@ -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 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(config.uploads.folder, { setHeaders }))
}
safe.use('/', express.static('./public', { setHeaders })) safe.use('/', express.static('./public', { setHeaders }))
safe.use('/', album) safe.use('/', album)
safe.use('/', nojs) safe.use('/', nojs)
safe.use('/api', api) safe.use('/api', api)
for (const page of config.pages) { for (const page of config.pages)
if (fs.existsSync(`./pages/custom/${page}.html`)) { if (fs.existsSync(`./pages/custom/${page}.html`)) {
safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, {
root: './pages/custom/' root: './pages/custom/'
@ -90,7 +89,6 @@ for (const page of config.pages) {
} else { } else {
safe.get(`/${page}`, (req, res, next) => res.render(page)) safe.get(`/${page}`, (req, res, next) => res.render(page))
} }
}
safe.use((req, res, next) => { safe.use((req, res, next) => {
res.status(404).sendFile(config.errorPages[404], { root: config.errorPages.rootDir }) res.status(404).sendFile(config.errorPages[404], { root: config.errorPages.rootDir })
@ -109,31 +107,31 @@ const start = async () => {
if (config.showGitHash) { if (config.showGitHash) {
const gitHash = await new Promise((resolve, reject) => { const gitHash = await new Promise((resolve, reject) => {
require('child_process').exec('git rev-parse HEAD', (error, stdout) => { require('child_process').exec('git rev-parse HEAD', (error, stdout) => {
if (error) { return reject(error) } if (error) return reject(error)
resolve(stdout.replace(/\n$/, '')) resolve(stdout.replace(/\n$/, ''))
}) })
}).catch(console.error) }).catch(console.error)
if (!gitHash) { return } if (!gitHash) return
console.log(`Git commit: ${gitHash}`) console.log(`Git commit: ${gitHash}`)
safe.set('git-hash', gitHash) safe.set('git-hash', gitHash)
} }
if (config.uploads.scan && config.uploads.scan.enabled) { if (config.uploads.scan && config.uploads.scan.enabled) {
const created = await new Promise(async (resolve, reject) => { 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')) 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) 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')) return reject(new Error('Could not ping clamd'))
}
const version = await clamd.version(config.uploads.scan.ip, config.uploads.scan.port).catch(reject) 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}`) console.log(`${config.uploads.scan.ip}:${config.uploads.scan.port} ${version}`)
const scanner = clamd.createScanner(config.uploads.scan.ip, config.uploads.scan.port) const scanner = clamd.createScanner(config.uploads.scan.ip, config.uploads.scan.port)
safe.set('clam-scanner', scanner) safe.set('clam-scanner', scanner)
return resolve(true) return resolve(true)
}).catch(error => console.error(error.toString())) }).catch(error => console.error(error.toString()))
if (!created) { return process.exit(1) } if (!created) return process.exit(1)
} }
if (config.uploads.cacheFileIdentifiers) { if (config.uploads.cacheFileIdentifiers) {
@ -142,24 +140,21 @@ const start = async () => {
const setSize = await new Promise((resolve, reject) => { const setSize = await new Promise((resolve, reject) => {
const uploadsDir = `./${config.uploads.folder}` const uploadsDir = `./${config.uploads.folder}`
fs.readdir(uploadsDir, (error, names) => { fs.readdir(uploadsDir, (error, names) => {
if (error) { return reject(error) } if (error) return reject(error)
const set = new Set() const set = new Set()
names.forEach(name => set.add(name.split('.')[0])) names.forEach(name => set.add(name.split('.')[0]))
safe.set('uploads-set', set) safe.set('uploads-set', set)
resolve(set.size) resolve(set.size)
}) })
}).catch(error => console.error(error.toString())) }).catch(error => console.error(error.toString()))
if (!setSize) { return process.exit(1) } if (!setSize) return process.exit(1)
process.stdout.write(` ${setSize} OK!\n`) process.stdout.write(` ${setSize} OK!\n`)
} }
safe.listen(config.port, () => { safe.listen(config.port, () => {
console.log(`lolisafe started on port ${config.port}`) console.log(`lolisafe started on port ${config.port}`)
if (process.env.DEV === '1') {
// DEV=1 yarn start // DEV=1 yarn start
console.log('lolisafe is in development mode, nunjucks caching disabled') if (process.env.DEV === '1') {
}
// Add readline interface to allow evaluating arbitrary JavaScript from console // Add readline interface to allow evaluating arbitrary JavaScript from console
readline.createInterface({ readline.createInterface({
input: process.stdin, input: process.stdin,
@ -167,7 +162,7 @@ const start = async () => {
prompt: '' prompt: ''
}).on('line', line => { }).on('line', line => {
try { try {
if (line === '.exit') { process.exit(0) } if (line === '.exit') process.exit(0)
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
process.stdout.write(`${require('util').inspect(eval(line), { depth: 0 })}\n`) process.stdout.write(`${require('util').inspect(eval(line), { depth: 0 })}\n`)
} catch (error) { } catch (error) {
@ -176,6 +171,8 @@ const start = async () => {
}).on('SIGINT', () => { }).on('SIGINT', () => {
process.exit(0) process.exit(0)
}) })
console.warn('development mode enabled (disabled nunjucks caching & enabled readline interface)')
}
}) })
} }

View File

@ -12,7 +12,8 @@
"rules": { "rules": {
"curly": [ "curly": [
"error", "error",
"multi" "multi",
"consistent"
], ],
"quotes": [ "quotes": [
"error", "error",

View File

@ -209,8 +209,9 @@ page.domClick = function (event) {
views.album = page.views.uploads.album views.album = page.views.uploads.album
views.all = page.views.uploads.all views.all = page.views.uploads.all
func = page.getUploads func = page.getUploads
} else if (page.currentView === 'users') } else if (page.currentView === 'users') {
func = page.getUsers func = page.getUsers
}
switch (action) { switch (action) {
case 'page-prev': case 'page-prev':
@ -785,8 +786,9 @@ page.deleteSelectedFiles = function () {
page.selected.uploads = page.selected.uploads.filter(function (id) { page.selected.uploads = page.selected.uploads.filter(function (id) {
return bulkdelete.data.failed.includes(id) return bulkdelete.data.failed.includes(id)
}) })
} else } else {
page.selected.uploads = [] page.selected.uploads = []
}
localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected.uploads) localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected.uploads)
@ -1801,10 +1803,11 @@ page.editUser = function (id) {
icon: 'success', icon: 'success',
content: div 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') swal('Success!', `${user.username} was renamed into: ${response.data.name}.`, 'success')
else } else {
swal('Success!', 'The user was edited!', 'success') swal('Success!', 'The user was edited!', 'success')
}
page.getUsers(page.views.users) page.getUsers(page.views.users)
}).catch(function (error) { }).catch(function (error) {
@ -1829,18 +1832,17 @@ page.disableUser = function (id) {
} }
} }
}).then(function (proceed) { }).then(function (proceed) {
if (!proceed) { return } if (!proceed) return
axios.post('api/users/disable', { id }).then(function (response) { axios.post('api/users/disable', { id }).then(function (response) {
if (!response) { return } if (!response) return
if (response.data.success === false) { if (response.data.success === false) {
if (response.data.description === 'No token provided') { if (response.data.description === 'No token provided')
return page.verifyToken(page.token) return page.verifyToken(page.token)
} else { else
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
}
swal('Success!', 'The user has been disabled.', 'success') swal('Success!', 'The user has been disabled.', 'success')
page.getUsers(page.views.users) page.getUsers(page.views.users)

View File

@ -121,9 +121,10 @@ page.prepareUpload = function () {
page.uploadUrls(this) page.uploadUrls(this)
}) })
page.setActiveTab('tab-files') page.setActiveTab('tab-files')
} else } else {
document.getElementById('tab-files').style.display = 'block' document.getElementById('tab-files').style.display = 'block'
} }
}
page.prepareAlbums = function () { page.prepareAlbums = function () {
const option = document.createElement('option') const option = document.createElement('option')
@ -296,7 +297,9 @@ page.uploadUrls = function (button) {
const previewsContainer = tabDiv.getElementsByClassName('uploads')[0] const previewsContainer = tabDiv.getElementsByClassName('uploads')[0]
const urls = document.getElementById('urls').value const urls = document.getElementById('urls').value
.split(/\r?\n/) .split(/\r?\n/)
.filter(function (url) { return url.trim().length }) .filter(function (url) {
return url.trim().length
})
document.getElementById('urls').value = urls.join('\n') document.getElementById('urls').value = urls.join('\n')
if (!urls.length) if (!urls.length)
@ -365,7 +368,11 @@ page.updateTemplate = function (file, response) {
const img = file.previewElement.querySelector('img') const img = file.previewElement.querySelector('img')
img.setAttribute('alt', response.name || '') img.setAttribute('alt', response.name || '')
img.dataset['src'] = response.url 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')) page.lazyLoad.update(file.previewElement.querySelectorAll('img'))
} }
} }

View File

@ -8,12 +8,11 @@ const homeDomain = config.homeDomain || config.domain
routes.get('/a/:identifier', async (req, res, next) => { routes.get('/a/:identifier', async (req, res, next) => {
const identifier = req.params.identifier const identifier = req.params.identifier
if (identifier === undefined) { if (identifier === undefined)
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
description: 'No identifier provided.' description: 'No identifier provided.'
}) })
}
const album = await db.table('albums') const album = await db.table('albums')
.where({ .where({
@ -22,14 +21,13 @@ routes.get('/a/:identifier', async (req, res, next) => {
}) })
.first() .first()
if (!album) { if (!album)
return res.status(404).sendFile('404.html', { root: './pages/error/' }) 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({ return res.status(401).json({
success: false, success: false,
description: 'This album is not available for public.' description: 'This album is not available for public.'
}) })
}
const files = await db.table('files') const files = await db.table('files')
.select('name', 'size') .select('name', 'size')
@ -42,19 +40,17 @@ routes.get('/a/:identifier', async (req, res, next) => {
let totalSize = 0 let totalSize = 0
for (const file of files) { for (const file of files) {
file.file = `${basedomain}/${file.name}` file.file = `${basedomain}/${file.name}`
totalSize += parseInt(file.size)
file.extname = path.extname(file.name).toLowerCase() file.extname = path.extname(file.name).toLowerCase()
if (utils.mayGenerateThumb(file.extname)) { if (utils.mayGenerateThumb(file.extname)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png` file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png`
/* /*
If thumbnail for album is still not set, do it. 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 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. 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', { return res.render('album', {

View File

@ -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.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', (req, res, next) => uploadController.upload(req, res, next))
routes.post('/upload/delete', (req, res, next) => uploadController.delete(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/bulkdelete', (req, res, next) => uploadController.bulkDelete(req, res, next))
routes.post('/upload/finishchunks', (req, res, next) => uploadController.finishChunks(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)) routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next))

View File

@ -15,13 +15,13 @@ thumbs.mayGenerateThumb = extname => {
thumbs.getFiles = directory => { thumbs.getFiles = directory => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readdir(directory, async (error, names) => { fs.readdir(directory, async (error, names) => {
if (error) { return reject(error) } if (error) return reject(error)
const files = [] const files = []
await Promise.all(names.map(name => { await Promise.all(names.map(name => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.lstat(path.join(directory, name), (error, stats) => { fs.lstat(path.join(directory, name), (error, stats) => {
if (error) { return reject(error) } if (error) return reject(error)
if (stats.isFile() && !name.startsWith('.')) { files.push(name) } if (stats.isFile() && !name.startsWith('.')) files.push(name)
resolve() resolve()
}) })
}) })
@ -62,16 +62,16 @@ thumbs.do = async () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const generate = async i => { const generate = async i => {
const _upload = _uploads[i] const _upload = _uploads[i]
if (!_upload) { return resolve() } if (!_upload) return resolve()
const extname = path.extname(_upload) const extname = path.extname(_upload)
const basename = _upload.slice(0, -extname.length) const basename = _upload.slice(0, -extname.length)
if (_thumbs.includes(basename) && !thumbs.force) { if (_thumbs.includes(basename) && !thumbs.force) {
if (thumbs.verbose) { console.log(`${_upload}: thumb exists.`) } if (thumbs.verbose) console.log(`${_upload}: thumb exists.`)
skipped++ skipped++
} else if (!thumbs.mayGenerateThumb(extname)) { } else if (!thumbs.mayGenerateThumb(extname)) {
if (thumbs.verbose) { console.log(`${_upload}: extension skipped.`) } if (thumbs.verbose) console.log(`${_upload}: extension skipped.`)
skipped++ skipped++
} else { } else {
const generated = await utils.generateThumbs(_upload, thumbs.force) const generated = await utils.generateThumbs(_upload, thumbs.force)