Updates (dashboard)

+ Better pagination.
+ Added more advanced filtering system in Manage Uploads.
It now supports filtering with multiple usernames and/or IPs.
It also supports refining the matches with wildcards.

Todo?
Perhaps add simple file name filtering for regular users in the future?
This commit is contained in:
Bobby Wibowo 2019-06-18 02:34:15 +07:00
parent ec6069d962
commit 06ac31d02e
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
3 changed files with 150 additions and 63 deletions

View File

@ -684,34 +684,86 @@ uploadsController.list = async (req, res) => {
// Headers is string-only, this seem to be the safest and lightest // Headers is string-only, this seem to be the safest and lightest
const all = req.headers.all === '1' const all = req.headers.all === '1'
const uploader = req.headers.uploader const filters = req.headers.filters
const ismoderator = perms.is(user, 'moderator') const ismoderator = perms.is(user, 'moderator')
if ((all || uploader) && !ismoderator) return res.status(403).end() if ((all || filters) && !ismoderator) return res.status(403).end()
const basedomain = config.domain const basedomain = config.domain
// For filtering by uploader's username // For filtering uploads
let uploaderID = null const _filters = {
if (uploader) { uploaders: [],
uploaderID = await db.table('users') names: [],
.where('username', uploader) ips: [],
flags: {
nouser: false,
noip: false
}
}
// Perhaps this can be simplified even further?
if (filters) {
const usernames = []
filters
.split(' ')
.map((v, i, a) => {
if (/[^\\]\\$/.test(v) && a[i + 1]) {
const tmp = `${v.slice(0, -1)} ${a[i + 1]}`
a[i + 1] = ''
return tmp
}
return v.replace(/\\\\/, '\\')
})
.map((v, i) => {
const x = v.indexOf(':')
if (x >= 0 && v.substring(x + 1))
return [v.substring(0, x), v.substring(x + 1)]
else if (v.startsWith('-'))
return [v]
})
.forEach(v => {
if (!v) return
if (v[0] === 'user') usernames.push(v[1])
else if (v[0] === 'name') _filters.names.push(v[1])
else if (v[0] === 'ip') _filters.ips.push(v[1])
else if (v[0] === '-user') _filters.flags.nouser = true
else if (v[0] === '-ip') _filters.flags.noip = true
})
_filters.uploaders = await db.table('users')
.whereIn('username', usernames)
.select('id') .select('id')
.first() .then(rows => rows.map(v => v.id))
.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 () { function filter () {
if (req.params.id === undefined) if (req.params.id !== undefined) {
this.where('id', '<>', '') // TODO: Why is this necessary?
else
this.where('albumid', req.params.id) this.where('albumid', req.params.id)
if (!all) } else if (!all) {
this.where('userid', user.id) this.where('userid', user.id)
else if (uploaderID) } else {
this.where('userid', uploaderID) // Fisrt, look for uploads matching ANY of the supplied 'user' OR 'ip' filters
// Then, refined the matches using the supplied 'name' filters
const raw = []
const source = []
if (_filters.uploaders.length)
source.push(`\`userid\` in (${_filters.uploaders.map(v => `'${v}'`).join(', ')})`)
if (_filters.ips.length)
source.push(`\`ip\` in (${_filters.ips.map(v => `'${v}'`).join(', ')})`)
if (_filters.flags.nouser)
source.push('(`userid` is null or \'\')')
if (_filters.flags.noip)
source.push('(`ip` is null or \'\')')
if (source.length)
raw.push(`(${source.join(' or ')})`)
if (_filters.names.length)
raw.push(`(${_filters.names.map(v => {
if (v.includes('*'))
return `\`name\` like '${v.replace(/\*/g, '%')}'`
else
return `\`name\` = '${v}'`
}).join(' or ')})`)
this.whereRaw(raw.join(' and '))
}
} }
// Query uploads count for pagination // Query uploads count for pagination
@ -763,32 +815,29 @@ uploadsController.list = async (req, res) => {
} }
// If we are a regular user, or we are not listing all uploads, send response // If we are a regular user, or we are not listing all uploads, send response
// TODO: !ismoderator is probably redundant (?)
if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain }) if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain })
// Otherwise proceed to querying usernames // 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 const userids = files
.map(file => file.userid) .map(file => file.userid)
.filter((v, i, a) => { .filter((v, i, a) => {
return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
}) })
// If there are no uploads attached to a registered user, send response // If there are no uploads attached to a registered user, send response
if (userids.length === 0) return res.json({ success: true, files, count, basedomain }) if (userids.length === 0) return res.json({ success: true, files, count, basedomain })
// Query usernames of user IDs from currently selected files // Query usernames of user IDs from currently selected files
users = await db.table('users') const users = await db.table('users')
.whereIn('id', userids) .whereIn('id', userids)
.select('id', 'username')
.then(rows => { .then(rows => {
// Build Object indexed by their IDs // Build Object indexed by their IDs
const obj = {} const obj = {}
for (const row of rows) obj[row.id] = row.username for (const row of rows) obj[row.id] = row.username
return obj return obj
}) })
}
return res.json({ success: true, files, count, users, basedomain }) return res.json({ success: true, files, count, users, basedomain })
} }

View File

@ -36,7 +36,7 @@ const page = {
// config of uploads view (all) // config of uploads view (all)
uploadsAll: { uploadsAll: {
type: localStorage[lsKeys.viewType.uploadsAll], type: localStorage[lsKeys.viewType.uploadsAll],
uploader: null, // uploader's name filters: null, // uploads' filters
pageNum: null, // page num pageNum: null, // page num
all: true all: true
}, },
@ -259,8 +259,10 @@ page.domClick = function (event) {
return page.editUser(id) return page.editUser(id)
case 'disable-user': case 'disable-user':
return page.disableUser(id) return page.disableUser(id)
case 'filter-by-uploader': case 'filters-help':
return page.filterByUploader(element) return page.filtersHelp(element)
case 'filter-uploads':
return page.filterUploads(element)
case 'view-user-uploads': case 'view-user-uploads':
return page.viewUserUploads(id) return page.viewUserUploads(id)
case 'page-prev': case 'page-prev':
@ -298,7 +300,7 @@ page.switchPage = function (action, element) {
func = page.getUploads func = page.getUploads
views.album = page.views[page.currentView].album views.album = page.views[page.currentView].album
views.all = page.views[page.currentView].all views.all = page.views[page.currentView].all
views.uploader = page.views[page.currentView].uploader views.filters = page.views[page.currentView].filters
} }
switch (action) { switch (action) {
@ -321,10 +323,10 @@ page.switchPage = function (action, element) {
} }
} }
page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { page.getUploads = function ({ pageNum, album, all, filters } = {}, element) {
if (element) page.isLoading(element, true) if (element) page.isLoading(element, true)
if ((all || uploader) && !page.permissions.moderator) if ((all || filters) && !page.permissions.moderator)
return swal('An error occurred!', 'You can not do this!', 'error') return swal('An error occurred!', 'You can not do this!', 'error')
if (typeof pageNum !== 'number' || pageNum < 0) if (typeof pageNum !== 'number' || pageNum < 0)
@ -336,7 +338,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const headers = {} const headers = {}
if (all) headers.all = '1' if (all) headers.all = '1'
if (uploader) headers.uploader = uploader if (filters) headers.filters = filters
axios.get(url, { headers }).then(function (response) { axios.get(url, { headers }).then(function (response) {
if (response.data.success === false) if (response.data.success === false)
if (response.data.description === 'No token provided') { if (response.data.description === 'No token provided') {
@ -360,16 +362,23 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const basedomain = response.data.basedomain const basedomain = response.data.basedomain
const pagination = page.paginate(response.data.count, 25, pageNum) const pagination = page.paginate(response.data.count, 25, pageNum)
let filter = '' let filter = '<div class="column is-hidden-mobile"></div>'
if (all) if (all)
filter = ` filter = `
<div class="column is-one-quarter"> <div class="column">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="uploader" class="input is-small" type="text" placeholder="Username" value="${uploader || ''}"> <input id="filters" class="input is-small" type="text" placeholder="Filters" value="${filters || ''}">
</div> </div>
<div class="control"> <div class="control">
<a class="button is-small is-breeze" title="Filter by uploader" data-action="filter-by-uploader"> <a class="button is-small is-breeze" title="Help?" data-action="filters-help">
<span class="icon">
<i class="icon-help-circled"></i>
</span>
</a>
</div>
<div class="control">
<a class="button is-small is-breeze" title="Filter uploads" data-action="filter-uploads">
<span class="icon"> <span class="icon">
<i class="icon-filter"></i> <i class="icon-filter"></i>
</span> </span>
@ -381,7 +390,6 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const extraControls = ` const extraControls = `
<div class="columns" style="margin-top: 10px"> <div class="columns" style="margin-top: 10px">
${filter} ${filter}
<div class="column is-hidden-mobile"></div>
<div class="column is-one-quarter"> <div class="column is-one-quarter">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
@ -456,8 +464,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
files[i].selected = page.selected[page.currentView].includes(files[i].id) files[i].selected = page.selected[page.currentView].includes(files[i].id)
if (allSelected && !files[i].selected) allSelected = false if (allSelected && !files[i].selected) allSelected = false
// Appendix (display album or user) // 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 (all) files[i].appendix = files[i].userid ? users[files[i].userid] : ''
else files[i].appendix = files[i].albumid ? albums[files[i].albumid] : ''
} }
if (page.views[page.currentView].type === 'thumbs') { if (page.views[page.currentView].type === 'thumbs') {
@ -594,7 +602,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
} }
if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploads') page.views.uploads.album = album
if (page.currentView === 'uploadsAll') page.views.uploadsAll.uploader = uploader if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters
page.views[page.currentView].pageNum = files.length ? pageNum : 0 page.views[page.currentView].pageNum = files.length ? pageNum : 0
}).catch(function (error) { }).catch(function (error) {
if (element) page.isLoading(element, false) if (element) page.isLoading(element, false)
@ -770,16 +778,46 @@ page.clearSelection = function () {
}) })
} }
page.filterByUploader = function (element) { page.filtersHelp = function (element) {
const uploader = document.getElementById('uploader').value const content = document.createElement('div')
page.getUploads({ all: true, uploader }, element) content.style = 'text-align: left'
content.innerHTML = `
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (file name).
Each key can be specified more than once.
Backlashes should be used if the usernames have spaces.
There are also 2 additional flags, namely <b>-user</b> and <b>-ip</b>, which will match uploads by non-registered users and have no IPs respectively.
How does it work?
First, it will filter uploads matching ANY of the supplied <b>user</b> or <b>ip</b> keys.
Then, it will refine the matches using the supplied <b>name</b> keys.
Examples:
Uploads from user with username "demo":
<span class="is-code">user:demo</span>
Uploads from users with username either "John Doe" OR "demo":
<span class="is-code">user:John\\ Doe user:demo</span>
Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
<span class="is-code">ip:127.0.0.1 name:*.rar name:*.zip</span>
Uploads from user with username "test" OR from non-registered users:
<span class="is-code">user:test -user</span>
`.trim().replace(/^ {6}/gm, '').replace(/\n/g, '<br>')
swal({ content })
}
page.filterUploads = function (element) {
const filters = document.getElementById('filters').value
page.getUploads({ all: true, filters }, element)
} }
page.viewUserUploads = function (id) { page.viewUserUploads = function (id) {
const user = page.cache.users[id] const user = page.cache.users[id]
if (!user) return if (!user) return
page.setActiveMenu(document.getElementById('itemManageUploads')) page.setActiveMenu(document.getElementById('itemManageUploads'))
page.getUploads({ all: true, uploader: user.username }) page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` })
} }
page.deleteFile = function (id) { page.deleteFile = function (id) {
@ -1956,17 +1994,17 @@ page.paginate = function (totalItems, itemsPerPage, currentPage) {
} }
} }
if (elementsToShow >= numPages) { if (elementsToShow + 1 >= numPages) {
add.pageNum(1, numPages) add.pageNum(1, numPages)
} else if (currentPage < elementsToShow) { } else if (currentPage < elementsToShow) {
add.pageNum(1, elementsToShow) add.pageNum(1, elementsToShow)
add.endDots() add.endDots()
} else if (currentPage > numPages - elementsToShow) { } else if (currentPage > numPages - elementsToShow + 1) {
add.startDots() add.startDots()
add.pageNum(numPages - elementsToShow, numPages) add.pageNum(numPages - elementsToShow + 1, numPages)
} else { } else {
add.startDots() add.startDots()
add.pageNum(currentPage - step, currentPage, step) add.pageNum(currentPage - step + 1, currentPage + step - 1)
add.endDots() add.endDots()
} }

View File

@ -16,7 +16,7 @@
v3: CSS and JS files (libs such as bulma, lazyload, etc). v3: CSS and JS files (libs such as bulma, lazyload, etc).
v4: Renders in /public/render/* directories (to be used by render.js). v4: Renders in /public/render/* directories (to be used by render.js).
#} #}
{% set v1 = "uDNOxxQGxC" %} {% set v1 = "sAfUXhJ9Gz" %}
{% set v2 = "hiboQUzAzp" %} {% set v2 = "hiboQUzAzp" %}
{% set v3 = "DKoamSTKbO" %} {% set v3 = "DKoamSTKbO" %}
{% set v4 = "43gxmxi7v8" %} {% set v4 = "43gxmxi7v8" %}