diff --git a/config.sample.js b/config.sample.js index 061f962..c20595a 100644 --- a/config.sample.js +++ b/config.sample.js @@ -207,6 +207,13 @@ module.exports = { chunkSize: 64 * 1024 }, + /* + Store uploader's IPs into the database. + NOTE: Dashboard's Manage Uploads will display IP column regardless of whether + this is set to true or false. + */ + storeIP: true, + /* Chunk size for chunk uploads. Needs to be in MB. If this is enabled, every files uploaded from the homepage uploader will forcibly be chunked diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 10fb1d0..923c010 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -545,7 +545,7 @@ uploadsController.formatInfoMap = (req, res, user, infoMap) => { type: info.data.mimetype, size: info.data.size, hash: fileHash, - ip: req.ip, + ip: config.uploads.storeIP !== false ? req.ip : null, // only disable if explicitly set to false albumid: albumsAuthorized[info.data.albumid] ? info.data.albumid : null, userid: user !== undefined ? user.id : null, timestamp: Math.floor(Date.now() / 1000) @@ -688,25 +688,33 @@ uploadsController.list = async (req, res) => { const ismoderator = perms.is(user, 'moderator') if ((all || uploader) && !ismoderator) return res.status(403).end() + const basedomain = config.domain + + // For filtering by uploader's username let uploaderID = null - if (uploader) + if (uploader) { uploaderID = await db.table('users') .where('username', uploader) .select('id') .first() .then(row => row ? row.id : null) + // Close request if the provided username is not valid + if (!uploaderID) + return res.json({ success: false, description: 'User with that username could not be found.' }) + } function filter () { if (req.params.id === undefined) - this.where('id', '<>', '') + this.where('id', '<>', '') // TODO: Why is this necessary? else this.where('albumid', req.params.id) - if (!ismoderator || !all) + if (!all) this.where('userid', user.id) else if (uploaderID) this.where('userid', uploaderID) } + // Query uploads count for pagination const count = await db.table('files') .where(filter) .count('id as count') @@ -716,55 +724,73 @@ uploadsController.list = async (req, res) => { let offset = req.params.page if (offset === undefined) offset = 0 + const columns = ['id', 'timestamp', 'name', 'userid', 'size'] + // Only select IPs if we are listing all uploads + columns.push(all ? 'ip' : 'albumid') + const files = await db.table('files') .where(filter) .orderBy('id', 'DESC') .limit(25) .offset(25 * offset) - .select('id', 'albumid', 'timestamp', 'name', 'userid', 'size') - - const albums = await db.table('albums') - .where(function () { - this.where('enabled', 1) - if (!all || !ismoderator) - this.where('userid', user.id) - }) - - const basedomain = config.domain - const userids = [] + .select(columns) for (const file of files) { - file.file = `${basedomain}/${file.name}` - - file.album = '' - if (file.albumid !== undefined) - for (const album of albums) - if (file.albumid === album.id) - file.album = album.name - - // Only push usernames if we are a moderator - if (all && ismoderator) - if (file.userid !== undefined && file.userid !== null && file.userid !== '') - userids.push(file.userid) - file.extname = utils.extname(file.name) if (utils.mayGenerateThumb(file.extname)) - file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png` + file.thumb = `/thumbs/${file.name.slice(0, -file.extname.length)}.png` } - // If we are a normal user, send response - if (!ismoderator) return res.json({ success: true, files, count }) + // If we are not listing all uploads, query album names + let albums = {} + if (!all) { + const albumids = files + .map(file => file.albumid) + .filter((v, i, a) => { + return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i + }) + albums = await db.table('albums') + .whereIn('id', albumids) + .where('enabled', 1) + .where('userid', user.id) + .select('id', 'name') + .then(rows => { + // Build Object indexed by their IDs + const obj = {} + for (const row of rows) obj[row.id] = row.name + return obj + }) + } - // If we are a moderator but there are no uploads attached to a user, send response - if (userids.length === 0) return res.json({ success: true, files, count }) + // If we are a regular user, or we are not listing all uploads, send response + if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain }) - const users = await db.table('users').whereIn('id', userids) - for (const dbUser of users) - for (const file of files) - if (file.userid === dbUser.id) - file.username = dbUser.username + // Otherwise proceed to querying usernames + let users = {} + if (uploaderID) { + // If we are already filtering by username, manually build array + users[uploaderID] = uploader + } else { + const userids = files + .map(file => file.userid) + .filter((v, i, a) => { + return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i + }) + // If there are no uploads attached to a registered user, send response + if (userids.length === 0) return res.json({ success: true, files, count, basedomain }) - return res.json({ success: true, files, count }) + // Query usernames of user IDs from currently selected files + users = await db.table('users') + .whereIn('id', userids) + .then(rows => { + // Build Object indexed by their IDs + const obj = {} + for (const row of rows) obj[row.id] = row.username + return obj + }) + } + + return res.json({ success: true, files, count, users, basedomain }) } module.exports = uploadsController diff --git a/public/js/dashboard.js b/public/js/dashboard.js index f6cf1bf..0a1d3c2 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -203,7 +203,7 @@ page.getItemID = function (element) { page.domClick = function (event) { // We are processing clicks this way to avoid using "onclick" attribute // Apparently we will need to use "unsafe-inline" for "script-src" directive - // of Content Security Policy (CSP), if we want ot use "onclick" attribute + // of Content Security Policy (CSP), if we want to use "onclick" attribute // Though I think that only applies to some browsers (?) // Either way, I personally would rather not // Of course it wouldn't have mattered if we didn't use CSP to begin with @@ -342,11 +342,12 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { if (response.data.description === 'No token provided') { return page.verifyToken(page.token) } else { + if (element) page.isLoading(element, false) return swal('An error occurred!', response.data.description, 'error') } - if (pageNum && (response.data.files.length === 0)) { - // Only remove loading class here, since beyond this the entire page will be replaced anyways + const files = response.data.files + if (pageNum && (files === 0)) { if (element) page.isLoading(element, false) return swal('An error occurred!', `There are no more uploads to populate page ${pageNum + 1}.`, 'error') } @@ -354,6 +355,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { page.currentView = all ? 'uploadsAll' : 'uploads' page.cache.uploads = {} + const albums = response.data.albums + const users = response.data.users + const basedomain = response.data.basedomain const pagination = page.paginate(response.data.count, 25, pageNum) let filter = '' @@ -432,7 +436,30 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { ` + // Set to true to tick "all files" checkbox in list view let allSelected = true + + for (let i = 0; i < files.length; i++) { + // Build full URLs + files[i].file = `${basedomain}/${files[i].name}` + if (files[i].thumb) files[i].thumb = `${basedomain}/${files[i].thumb}` + // Cache bare minimum data for thumbnails viewer + page.cache.uploads[files[i].id] = { + name: files[i].name, + thumb: files[i].thumb, + original: files[i].file + } + // Prettify + files[i].prettyBytes = page.getPrettyBytes(parseInt(files[i].size)) + files[i].prettyDate = page.getPrettyDate(new Date(files[i].timestamp * 1000)) + // Update selected status + files[i].selected = page.selected[page.currentView].includes(files[i].id) + if (allSelected && !files[i].selected) allSelected = false + // Appendix (display album or user) + files[i].appendix = files[i].albumid ? albums[files[i].albumid] : '' + if (all) files[i].appendix = files[i].userid ? users[files[i].userid] : '' + } + if (page.views[page.currentView].type === 'thumbs') { page.dom.innerHTML = ` ${pagination} @@ -447,24 +474,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { const table = document.getElementById('table') - for (let i = 0; i < response.data.files.length; i++) { - const upload = response.data.files[i] - const selected = page.selected[page.currentView].includes(upload.id) - if (!selected && allSelected) allSelected = false - - page.cache.uploads[upload.id] = { - name: upload.name, - thumb: upload.thumb, - original: upload.file - } - - // Prettify - upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size)) - upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000)) - - let displayAlbumOrUser = upload.album - if (all) displayAlbumOrUser = upload.username || '' - + for (let i = 0; i < files.length; i++) { + const upload = files[i] const div = document.createElement('div') div.className = 'image-container column is-narrow' div.dataset.id = upload.id @@ -474,7 +485,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { div.innerHTML = `

${upload.extname || 'N/A'}

` div.innerHTML += ` - +
@@ -499,7 +510,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {

${upload.name}

-

${displayAlbumOrUser ? `${displayAlbumOrUser} – ` : ''}${upload.prettyBytes}

+

${upload.appendix ? `${upload.appendix} – ` : ''}${upload.prettyBytes}

` @@ -508,9 +519,6 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { page.lazyLoad.update() } } else { - let albumOrUser = 'Album' - if (all) albumOrUser = 'User' - page.dom.innerHTML = ` ${pagination} ${extraControls} @@ -521,8 +529,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { File - ${albumOrUser} + ${all ? 'User' : 'Album'} Size + ${all ? 'IP' : ''} Date @@ -538,31 +547,16 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { const table = document.getElementById('table') - for (let i = 0; i < response.data.files.length; i++) { - const upload = response.data.files[i] - const selected = page.selected[page.currentView].includes(upload.id) - if (!selected && allSelected) allSelected = false - - page.cache.uploads[upload.id] = { - name: upload.name, - thumb: upload.thumb, - original: upload.file - } - - // Prettify - upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size)) - upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000)) - - let displayAlbumOrUser = upload.album - if (all) displayAlbumOrUser = upload.username || '' - + for (let i = 0; i < files.length; i++) { + const upload = files[i] const tr = document.createElement('tr') tr.dataset.id = upload.id tr.innerHTML = ` - +
${upload.name} - ${displayAlbumOrUser} + ${upload.appendix} ${upload.prettyBytes} + ${all ? `${upload.ip || ''}` : ''} ${upload.prettyDate} @@ -594,14 +588,14 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { } } - if (allSelected && response.data.files.length) { + if (allSelected && files.length) { const selectAll = document.getElementById('selectAll') if (selectAll) selectAll.checked = true } if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploadsAll') page.views.uploadsAll.uploader = uploader - page.views[page.currentView].pageNum = response.data.files.length ? pageNum : 0 + page.views[page.currentView].pageNum = files.length ? pageNum : 0 }).catch(function (error) { if (element) page.isLoading(element, false) console.log(error) diff --git a/views/_globals.njk b/views/_globals.njk index 068b465..bdf7f2a 100644 --- a/views/_globals.njk +++ b/views/_globals.njk @@ -16,7 +16,7 @@ v3: CSS and JS files (libs such as bulma, lazyload, etc). v4: Renders in /public/render/* directories (to be used by render.js). #} -{% set v1 = "WNcjDAmPAR" %} +{% set v1 = "uDNOxxQGxC" %} {% set v2 = "hiboQUzAzp" %} {% set v3 = "DKoamSTKbO" %} {% set v4 = "43gxmxi7v8" %}