mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-18 17:21:33 +00:00
Massively overhauled uploads filtering endpoint
Please consult the Help? button again to learn all the syntax changes! The prompt will now also have its width expanded! Updated dependency, knex: 0.20.13 -> 0.20.15. Added new dependency: search-query-parser. Updated all sub-dependencies. Critical? Admins-only API /users/edit will no longer return NEW password salt of the user when randomizing their password. Added page.escape() function to js/misc/utils.js. This will be used to escape input in upload filters input box. The same function used in utilsController.js. Pretty dates will now use / instead of - for date separator. This is due to the fact that date range key for filtering uploads can not accepts dates with - separator. To avoid inconsistency, we will now use / separator. Caching system of album public pages will now be disabled during development (yarn develop). Cleaned up domClick() function in js/dashboard.js. If using date or expiry range keys when filtering uploads, attach client's timezone offset to the API requets. This will be used by the server to calculate timezone differences. Success prompt when changing token will now auto-close. Removed ID column from Manage Users. Improved success prompt when editing users. This will properly list all of the edited fields at once, excluding user group change. Success message for user group change will require a bit more changes on the API endpoint, which is a bit annoying. Rebuilt client-side assets and bumped v1 version string.
This commit is contained in:
parent
1c260c87b0
commit
3e3878b93c
@ -259,7 +259,7 @@ self.editUser = async (req, res, next) => {
|
||||
utils.invalidateStatsCache('users')
|
||||
|
||||
const response = { success: true, update }
|
||||
if (password) response.password = password
|
||||
if (password) response.update.password = password
|
||||
return res.json(response)
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
|
@ -4,6 +4,7 @@ const fs = require('fs')
|
||||
const multer = require('multer')
|
||||
const path = require('path')
|
||||
const randomstring = require('randomstring')
|
||||
const searchQuery = require('search-query-parser')
|
||||
const paths = require('./pathsController')
|
||||
const perms = require('./permissionController')
|
||||
const utils = require('./utilsController')
|
||||
@ -758,94 +759,172 @@ self.list = async (req, res) => {
|
||||
|
||||
const all = Boolean(req.headers.all)
|
||||
const filters = req.headers.filters
|
||||
const minoffset = req.headers.minoffset
|
||||
const ismoderator = perms.is(user, 'moderator')
|
||||
if ((all || filters) && !ismoderator)
|
||||
return res.status(403).end()
|
||||
|
||||
const basedomain = config.domain
|
||||
|
||||
// For filtering uploads
|
||||
const _filters = {
|
||||
const filterObj = {
|
||||
uploaders: [],
|
||||
names: [],
|
||||
ips: [],
|
||||
flags: {
|
||||
nouser: false,
|
||||
noip: false
|
||||
excludeUploaders: [],
|
||||
queries: {
|
||||
exclude: {}
|
||||
},
|
||||
keywords: []
|
||||
flags: {}
|
||||
}
|
||||
|
||||
// Cast column(s) to specific type if they're stored differently
|
||||
const _orderByCasts = {
|
||||
size: 'integer'
|
||||
const orderByObj = {
|
||||
// Cast columns to specific type if they are stored differently
|
||||
casts: {
|
||||
size: 'integer'
|
||||
},
|
||||
// Columns mapping
|
||||
maps: {
|
||||
date: 'timestamp',
|
||||
expiry: 'expirydate'
|
||||
},
|
||||
// Columns with which to use SQLite's NULLS LAST option
|
||||
nullsLast: [
|
||||
'userid',
|
||||
'expirydate',
|
||||
'ip'
|
||||
],
|
||||
parsed: []
|
||||
}
|
||||
// Columns with which to use SQLite's NULLS LAST option
|
||||
const _orderByNullsLast = [
|
||||
'userid',
|
||||
'expirydate',
|
||||
'ip'
|
||||
]
|
||||
const _orderBy = []
|
||||
|
||||
// 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
|
||||
return v
|
||||
})
|
||||
.forEach(v => {
|
||||
if (Array.isArray(v)) {
|
||||
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] === 'orderby') {
|
||||
const tmp = v[1].split(':')
|
||||
let col = tmp[0]
|
||||
let dir = 'asc'
|
||||
if (_orderByCasts[col])
|
||||
col = `cast (\`${col}\` as ${_orderByCasts[col]})`
|
||||
if (tmp[1] && /^d/i.test(tmp[1]))
|
||||
dir = 'desc'
|
||||
_orderBy.push(`${col} ${dir}${_orderByNullsLast.includes(col) ? ' nulls last' : ''}`)
|
||||
}
|
||||
} else {
|
||||
if (v === '-user')
|
||||
_filters.flags.nouser = true
|
||||
else if (v === '-ip')
|
||||
_filters.flags.noip = true
|
||||
else
|
||||
_filters.keywords.push(v[0])
|
||||
}
|
||||
})
|
||||
_filters.uploaders = await db.table('users')
|
||||
.whereIn('username', usernames)
|
||||
.select('id', 'username')
|
||||
}
|
||||
const keywords = [
|
||||
'ip',
|
||||
'user'
|
||||
]
|
||||
const ranges = [
|
||||
'date',
|
||||
'expiry'
|
||||
]
|
||||
filterObj.queries = searchQuery.parse(filters, {
|
||||
keywords: keywords.concat([
|
||||
'orderby'
|
||||
]),
|
||||
ranges,
|
||||
tokenize: true,
|
||||
alwaysArray: true,
|
||||
offsets: false
|
||||
})
|
||||
|
||||
if (filters && !(_filters.uploaders.length || _filters.names.length || _filters.ips.length || _filters.flags.nouser || _filters.flags.noip || _orderBy.length))
|
||||
if (_filters.keywords.length)
|
||||
// TODO: Support filtering using keywords only
|
||||
return res.json({ success: false, description: 'Filtering using keywords only is still work in progress. Please confirm valid filtering keys through the Help? button!' })
|
||||
else
|
||||
return res.json({ success: false, description: 'No valid filter or sort keys were used. Please confirm the valid keys through the Help? button!' })
|
||||
for (const key of keywords)
|
||||
if (filterObj.queries[key]) {
|
||||
// Make sure keyword arrays only contain unique values
|
||||
filterObj.queries[key] = filterObj.queries[key].filter((v, i, a) => a.indexOf(v) === i)
|
||||
|
||||
// Flag to match NULL values
|
||||
const index = filterObj.queries[key].indexOf('-')
|
||||
if (index !== -1) {
|
||||
filterObj.flags[`no${key}`] = true
|
||||
filterObj.queries[key].splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const parseDate = (date, minoffset, resetMs) => {
|
||||
// [YYYY][/MM][/DD] [HH][:MM][:SS]
|
||||
// e.g. 2020/01/01 00:00:00, 2018/01/01 06, 2019/11, 12:34:00
|
||||
const match = date.match(/^(\d{4})?(\/\d{2})?(\/\d{2})?\s?(\d{2})?(:\d{2})?(:\d{2})?$/)
|
||||
|
||||
if (match) {
|
||||
const offset = 60000 * (utils.timezoneOffset - minoffset)
|
||||
const dateObj = new Date(Date.now() + offset)
|
||||
|
||||
if (match[1] !== undefined)
|
||||
dateObj.setFullYear(Number(match[1]), // full year
|
||||
match[2] !== undefined ? (Number(match[2].slice(1)) - 1) : 0, // month, zero-based
|
||||
match[3] !== undefined ? Number(match[3].slice(1)) : 1) // date
|
||||
|
||||
if (match[4] !== undefined)
|
||||
dateObj.setHours(Number(match[4]), // hours
|
||||
match[5] !== undefined ? Number(match[5].slice(1)) : 0, // minutes
|
||||
match[6] !== undefined ? Number(match[6].slice(1)) : 0) // seconds
|
||||
|
||||
if (resetMs)
|
||||
dateObj.setMilliseconds(0)
|
||||
|
||||
// Calculate timezone differences
|
||||
const newDateObj = new Date(dateObj.getTime() - offset)
|
||||
return newDateObj
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Parse dates to timestamps
|
||||
for (const range of ranges)
|
||||
if (filterObj.queries[range]) {
|
||||
if (filterObj.queries[range].from) {
|
||||
const parsed = parseDate(filterObj.queries[range].from, minoffset, true)
|
||||
filterObj.queries[range].from = parsed ? Math.floor(parsed / 1000) : null
|
||||
}
|
||||
if (filterObj.queries[range].to) {
|
||||
const parsed = parseDate(filterObj.queries[range].to, minoffset, true)
|
||||
filterObj.queries[range].to = parsed ? Math.ceil(parsed / 1000) : null
|
||||
}
|
||||
}
|
||||
|
||||
// Query users table for user IDs
|
||||
if (filterObj.queries.user || filterObj.queries.exclude.user) {
|
||||
const usernames = []
|
||||
.concat(filterObj.queries.user || [])
|
||||
.concat(filterObj.queries.exclude.user || [])
|
||||
|
||||
const uploaders = await db.table('users')
|
||||
.whereIn('username', usernames)
|
||||
.select('id', 'username')
|
||||
|
||||
// If no matches, or mismatched results
|
||||
if (!uploaders || (uploaders.length !== usernames.length)) {
|
||||
const notFound = usernames.filter(username => {
|
||||
return !uploaders.find(uploader => uploader.username === username)
|
||||
})
|
||||
if (notFound)
|
||||
return res.json({
|
||||
success: false,
|
||||
description: `User${notFound.length === 1 ? '' : 's'} not found: ${notFound.join(', ')}.`
|
||||
})
|
||||
}
|
||||
|
||||
for (const uploader of uploaders)
|
||||
if (filterObj.queries.user && filterObj.queries.user.includes(uploader.username))
|
||||
filterObj.uploaders.push(uploader)
|
||||
else
|
||||
filterObj.excludeUploaders.push(uploader)
|
||||
|
||||
delete filterObj.queries.user
|
||||
delete filterObj.queries.exclude.user
|
||||
}
|
||||
|
||||
// Parse orderby keys
|
||||
if (filterObj.queries.orderby) {
|
||||
for (const obQuery of filterObj.queries.orderby) {
|
||||
const tmp = obQuery.toLowerCase().split(':')
|
||||
|
||||
let column = orderByObj.maps[tmp[0]] || tmp[0]
|
||||
let direction = 'asc'
|
||||
|
||||
if (orderByObj.casts[column])
|
||||
column = `cast (\`${column}\` as ${orderByObj.casts[column]})`
|
||||
if (tmp[1] && /^d/.test(tmp[1]))
|
||||
direction = 'desc'
|
||||
|
||||
const suffix = orderByObj.nullsLast.includes(column) ? ' nulls last' : ''
|
||||
orderByObj.parsed.push(`${column} ${direction}${suffix}`)
|
||||
}
|
||||
|
||||
delete filterObj.queries.orderby
|
||||
}
|
||||
|
||||
// For some reason, single value won't be in Array even with 'alwaysArray' option
|
||||
if (typeof filterObj.queries.exclude.text === 'string')
|
||||
filterObj.queries.exclude.text = [filterObj.queries.exclude.text]
|
||||
}
|
||||
|
||||
function filter () {
|
||||
if (req.params.id !== undefined)
|
||||
@ -853,23 +932,53 @@ self.list = async (req, res) => {
|
||||
else if (!all)
|
||||
this.where('userid', user.id)
|
||||
else
|
||||
// Fisrt, look for uploads matching ANY of the supplied 'user' OR 'ip' filters
|
||||
// Then, refine the matches using the supplied 'name' filters
|
||||
// Sheesh, these look too overbearing...
|
||||
this.where(function () {
|
||||
if (_filters.uploaders.length)
|
||||
this.orWhereIn('userid', _filters.uploaders.map(v => v.id))
|
||||
if (_filters.ips.length)
|
||||
this.orWhereIn('ip', _filters.ips)
|
||||
if (_filters.flags.nouser)
|
||||
// Filter uploads matching any of the supplied 'user' keys and/or NULL flag
|
||||
if (filterObj.uploaders.length)
|
||||
this.orWhereIn('userid', filterObj.uploaders.map(v => v.id))
|
||||
if (filterObj.excludeUploaders.length)
|
||||
this.orWhereNotIn('userid', filterObj.excludeUploaders.map(v => v.id))
|
||||
if (filterObj.flags.nouser)
|
||||
this.orWhereNull('userid')
|
||||
if (_filters.flags.noip)
|
||||
}).orWhere(function () {
|
||||
// Filter uploads matching any of the supplied 'ip' keys and/or NULL flag
|
||||
if (filterObj.queries.ip)
|
||||
this.orWhereIn('ip', filterObj.queries.ip)
|
||||
if (filterObj.queries.exclude.ip)
|
||||
this.orWhereNotIn('ip', filterObj.queries.exclude.ip)
|
||||
if (filterObj.flags.noip)
|
||||
this.orWhereNull('ip')
|
||||
}).andWhere(function () {
|
||||
for (const name of _filters.names)
|
||||
// Then, refine using the supplied 'date' and/or 'expiry' ranges
|
||||
if (filterObj.queries.date)
|
||||
if (typeof filterObj.queries.date.to === 'number')
|
||||
this.andWhereBetween('timestamp', [filterObj.queries.date.from, filterObj.queries.date.to])
|
||||
else
|
||||
this.andWhere('timestamp', '>=', filterObj.queries.date.from)
|
||||
if (filterObj.queries.expiry)
|
||||
if (typeof filterObj.queries.expiry.to === 'number')
|
||||
this.andWhereBetween('expirydate', [filterObj.queries.expiry.from, filterObj.queries.expiry.to])
|
||||
else
|
||||
this.andWhere('expirydate', '>=', filterObj.queries.date.from)
|
||||
}).andWhere(function () {
|
||||
// Then, refine using the supplied keywords against their file names
|
||||
if (!filterObj.queries.text) return
|
||||
for (const name of filterObj.queries.text)
|
||||
if (name.includes('*'))
|
||||
this.orWhere('name', 'like', name.replace(/\*/g, '%'))
|
||||
else
|
||||
this.orWhere('name', name)
|
||||
// If no asterisks, assume partial
|
||||
this.orWhere('name', 'like', `%${name}%`)
|
||||
}).andWhere(function () {
|
||||
// Finally, refine using the supplied exclusions against their file names
|
||||
if (!filterObj.queries.exclude.text) return
|
||||
for (const exclude of filterObj.queries.exclude.text)
|
||||
if (exclude.includes('*'))
|
||||
this.orWhere('name', 'not like', exclude.replace(/\*/g, '%'))
|
||||
else
|
||||
// If no asterisks, assume partial
|
||||
this.orWhere('name', 'not like', `%${exclude}%`)
|
||||
})
|
||||
}
|
||||
|
||||
@ -886,16 +995,18 @@ self.list = async (req, res) => {
|
||||
if (offset === undefined) offset = 0
|
||||
|
||||
const columns = ['id', 'name', 'userid', 'size', 'timestamp']
|
||||
|
||||
if (temporaryUploads)
|
||||
columns.push('expirydate')
|
||||
|
||||
// Only select IPs if we are listing all uploads
|
||||
columns.push(all ? 'ip' : 'albumid')
|
||||
|
||||
const orderByRaw = orderByObj.parsed.length
|
||||
? orderByObj.parsed.join(', ')
|
||||
: '`id` desc'
|
||||
const files = await db.table('files')
|
||||
.where(filter)
|
||||
.orderByRaw(_orderBy.length ? _orderBy.join(', ') : '`id` desc')
|
||||
.orderByRaw(orderByRaw)
|
||||
.limit(25)
|
||||
.offset(25 * offset)
|
||||
.select(columns)
|
||||
@ -923,7 +1034,7 @@ self.list = async (req, res) => {
|
||||
.where('userid', user.id)
|
||||
.select('id', 'name')
|
||||
.then(rows => {
|
||||
// Build Object indexed by their IDs
|
||||
// Build Object indexed by their IDs
|
||||
const obj = {}
|
||||
for (const row of rows)
|
||||
obj[row.id] = row.name
|
||||
@ -936,8 +1047,8 @@ self.list = async (req, res) => {
|
||||
return res.json({ success: true, files, count, albums, basedomain })
|
||||
|
||||
// Otherwise proceed to querying usernames
|
||||
let _users = _filters.uploaders
|
||||
if (!_users.length) {
|
||||
let usersTable = filterObj.uploaders
|
||||
if (!usersTable.length) {
|
||||
const userids = files
|
||||
.map(file => file.userid)
|
||||
.filter((v, i, a) => {
|
||||
@ -949,13 +1060,13 @@ self.list = async (req, res) => {
|
||||
return res.json({ success: true, files, count, basedomain })
|
||||
|
||||
// Query usernames of user IDs from currently selected files
|
||||
_users = await db.table('users')
|
||||
usersTable = await db.table('users')
|
||||
.whereIn('id', userids)
|
||||
.select('id', 'username')
|
||||
}
|
||||
|
||||
const users = {}
|
||||
for (const user of _users)
|
||||
for (const user of usersTable)
|
||||
users[user.id] = user.username
|
||||
|
||||
return res.json({ success: true, files, count, users, basedomain })
|
||||
|
@ -31,7 +31,8 @@ const self = {
|
||||
|
||||
ffprobe: promisify(ffmpeg.ffprobe),
|
||||
|
||||
albumsCache: {}
|
||||
albumsCache: {},
|
||||
timezoneOffset: new Date().getTimezoneOffset()
|
||||
}
|
||||
|
||||
const statsCache = {
|
||||
@ -106,7 +107,7 @@ self.extname = filename => {
|
||||
return extname + multi
|
||||
}
|
||||
|
||||
self.escape = (string) => {
|
||||
self.escape = string => {
|
||||
// MIT License
|
||||
// Copyright(c) 2012-2013 TJ Holowaychuk
|
||||
// Copyright(c) 2015 Andreas Lubbe
|
||||
|
2
dist/js/dashboard.js
vendored
2
dist/js/dashboard.js
vendored
File diff suppressed because one or more lines are too long
2
dist/js/dashboard.js.map
vendored
2
dist/js/dashboard.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/misc/utils.js
vendored
2
dist/js/misc/utils.js
vendored
@ -1,2 +1,2 @@
|
||||
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=page.token?{token:page.token||"",albumid:page.album||""}:{};e.filelength=page.fileLength||"",e.age=page.uploadAge||"",e.striptags=page.stripTags||"";for(var t=[],a=Object.keys(e),n=0;n<a.length;n++)t.push(' "'+a[n]+'": "'+e[a[n]]+'"');var o=(location.hostname+location.pathname).replace(/\/(dashboard)?$/,""),r=o.replace(/\//g,"_"),i=document.querySelector("#ShareX"),s='{\n "Name": "'+r+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+location.protocol+"//"+o+'/api/upload",\n "Headers": {\n'+t.join(",\n")+'\n },\n "Body": "MultipartFormData",\n "FileFormName": "files[]",\n "URL": "$json:files[0].url$",\n "ThumbnailURL": "$json:files[0].url$"\n}',l=new Blob([s],{type:"application/octet-binary"});i.setAttribute("href",URL.createObjectURL(l)),i.setAttribute("download",r+".sxcu")},page.getPrettyDate=function(e){return e.getFullYear()+"-"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"-"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",n=t?1e3:1024;if(a&&(e=-e),e<n)return""+a+e+" B";var o=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(n,o)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(o-1)+(t?"":"i"))+"B"};
|
||||
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=page.token?{token:page.token||"",albumid:page.album||""}:{};e.filelength=page.fileLength||"",e.age=page.uploadAge||"",e.striptags=page.stripTags||"";for(var t=[],a=Object.keys(e),r=0;r<a.length;r++)t.push(' "'+a[r]+'": "'+e[a[r]]+'"');var n=(location.hostname+location.pathname).replace(/\/(dashboard)?$/,""),o=n.replace(/\//g,"_"),i=document.querySelector("#ShareX"),s='{\n "Name": "'+o+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+location.protocol+"//"+n+'/api/upload",\n "Headers": {\n'+t.join(",\n")+'\n },\n "Body": "MultipartFormData",\n "FileFormName": "files[]",\n "URL": "$json:files[0].url$",\n "ThumbnailURL": "$json:files[0].url$"\n}',l=new Blob([s],{type:"application/octet-binary"});i.setAttribute("href",URL.createObjectURL(l)),i.setAttribute("download",o+".sxcu")},page.getPrettyDate=function(e){return e.getFullYear()+"/"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"/"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",r=t?1e3:1024;if(a&&(e=-e),e<r)return""+a+e+" B";var n=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(r,n)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(n-1)+(t?"":"i"))+"B"},page.escape=function(e){if(!e)return e;var t,a=String(e),r=/["'&<>]/.exec(a);if(!r)return a;var n="",o=0,i=0;for(o=r.index;o<a.length;o++){switch(a.charCodeAt(o)){case 34:t=""";break;case 38:t="&";break;case 39:t="'";break;case 60:t="<";break;case 62:t=">";break;default:continue}i!==o&&(n+=a.substring(i,o)),i=o+1,n+=t}return i!==o?n+a.substring(i,o):n};
|
||||
//# sourceMappingURL=utils.js.map
|
||||
|
2
dist/js/misc/utils.js.map
vendored
2
dist/js/misc/utils.js.map
vendored
File diff suppressed because one or more lines are too long
@ -37,12 +37,13 @@
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"helmet": "^3.22.0",
|
||||
"jszip": "^3.3.0",
|
||||
"knex": "^0.20.13",
|
||||
"knex": "^0.20.15",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"nunjucks": "^3.2.1",
|
||||
"randomstring": "^1.1.5",
|
||||
"readline": "^1.3.0",
|
||||
"search-query-parser": "^1.5.5",
|
||||
"sharp": "^0.25.2",
|
||||
"sqlite3": "^4.1.1",
|
||||
"systeminformation": "^4.23.3"
|
||||
|
@ -31,30 +31,33 @@ routes.get('/a/:identifier', async (req, res, next) => {
|
||||
|
||||
const nojs = req.query.nojs !== undefined
|
||||
|
||||
// Cache ID - we initialize a separate cache for No-JS version
|
||||
const cacheid = nojs ? `${album.id}-nojs` : album.id
|
||||
let cacheid
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
// Cache ID - we initialize a separate cache for No-JS version
|
||||
cacheid = nojs ? `${album.id}-nojs` : album.id
|
||||
|
||||
if (!utils.albumsCache[cacheid])
|
||||
utils.albumsCache[cacheid] = {
|
||||
cache: null,
|
||||
generating: false,
|
||||
// Cache will actually be deleted after the album has been updated,
|
||||
// so storing this timestamp may be redundant, but just in case.
|
||||
generatedAt: 0
|
||||
}
|
||||
if (!utils.albumsCache[cacheid])
|
||||
utils.albumsCache[cacheid] = {
|
||||
cache: null,
|
||||
generating: false,
|
||||
// Cache will actually be deleted after the album has been updated,
|
||||
// so storing this timestamp may be redundant, but just in case.
|
||||
generatedAt: 0
|
||||
}
|
||||
|
||||
if (!utils.albumsCache[cacheid].cache && utils.albumsCache[cacheid].generating)
|
||||
return res.json({
|
||||
success: false,
|
||||
description: 'This album is still generating its public page.'
|
||||
})
|
||||
else if ((album.editedAt < utils.albumsCache[cacheid].generatedAt) || utils.albumsCache[cacheid].generating)
|
||||
return res.send(utils.albumsCache[cacheid].cache)
|
||||
if (!utils.albumsCache[cacheid].cache && utils.albumsCache[cacheid].generating)
|
||||
return res.json({
|
||||
success: false,
|
||||
description: 'This album is still generating its public page.'
|
||||
})
|
||||
else if ((album.editedAt < utils.albumsCache[cacheid].generatedAt) || utils.albumsCache[cacheid].generating)
|
||||
return res.send(utils.albumsCache[cacheid].cache)
|
||||
|
||||
// Use current timestamp to make sure cache is invalidated
|
||||
// when an album is edited during this generation process.
|
||||
utils.albumsCache[cacheid].generating = true
|
||||
utils.albumsCache[cacheid].generatedAt = Math.floor(Date.now() / 1000)
|
||||
// Use current timestamp to make sure cache is invalidated
|
||||
// when an album is edited during this generation process.
|
||||
utils.albumsCache[cacheid].generating = true
|
||||
utils.albumsCache[cacheid].generatedAt = Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const files = await db.table('files')
|
||||
.select('name', 'size')
|
||||
@ -89,12 +92,15 @@ routes.get('/a/:identifier', async (req, res, next) => {
|
||||
files,
|
||||
nojs
|
||||
}, (error, html) => {
|
||||
utils.albumsCache[cacheid].cache = error ? null : html
|
||||
utils.albumsCache[cacheid].generating = false
|
||||
const data = error ? null : html
|
||||
if (cacheid) {
|
||||
utils.albumsCache[cacheid].cache = data
|
||||
utils.albumsCache[cacheid].generating = false
|
||||
}
|
||||
|
||||
// Express should already send error to the next handler
|
||||
if (error) return
|
||||
return res.send(utils.albumsCache[cacheid].cache)
|
||||
return res.send(data)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -298,34 +298,34 @@ page.domClick = event => {
|
||||
const action = element.dataset.action
|
||||
|
||||
switch (action) {
|
||||
// Uploads
|
||||
case 'view-list':
|
||||
return page.setUploadsView('list', element)
|
||||
case 'view-thumbs':
|
||||
return page.setUploadsView('thumbs', element)
|
||||
case 'clear-selection':
|
||||
return page.clearSelection()
|
||||
case 'add-selected-uploads-to-album':
|
||||
return page.addSelectedUploadsToAlbum()
|
||||
case 'select':
|
||||
return page.select(element, event)
|
||||
case 'select-all':
|
||||
return page.selectAll(element)
|
||||
case 'add-to-album':
|
||||
return page.addToAlbum(id)
|
||||
case 'delete-upload':
|
||||
return page.deleteUpload(id)
|
||||
case 'add-selected-uploads-to-album':
|
||||
return page.addSelectedUploadsToAlbum()
|
||||
case 'bulk-delete-uploads':
|
||||
return page.bulkDeleteUploads()
|
||||
case 'display-preview':
|
||||
return page.displayPreview(id)
|
||||
// Manage uploads
|
||||
case 'upload-filters-help':
|
||||
return page.uploadFiltersHelp(element)
|
||||
case 'filter-uploads':
|
||||
return page.filterUploads(element)
|
||||
// Manage your albums
|
||||
case 'submit-album':
|
||||
return page.submitAlbum(element)
|
||||
case 'edit-album':
|
||||
return page.editAlbum(id)
|
||||
case 'delete-album':
|
||||
return page.deleteAlbum(id)
|
||||
case 'get-new-token':
|
||||
return page.getNewToken(element)
|
||||
// Manage users
|
||||
case 'create-user':
|
||||
return page.createUser()
|
||||
case 'edit-user':
|
||||
@ -334,12 +334,24 @@ page.domClick = event => {
|
||||
return page.disableUser(id)
|
||||
case 'delete-user':
|
||||
return page.deleteUser(id)
|
||||
case 'user-filters-help':
|
||||
return page.userFiltersHelp(element)
|
||||
case 'filter-uploads':
|
||||
return page.filterUploads(element)
|
||||
case 'view-user-uploads':
|
||||
return page.viewUserUploads(id, element)
|
||||
/* // WIP
|
||||
case 'user-filters-help':
|
||||
return page.userFiltersHelp(element)
|
||||
case 'filter-users':
|
||||
return page.filterUsers(element)
|
||||
*/
|
||||
// Others
|
||||
case 'get-new-token':
|
||||
return page.getNewToken(element)
|
||||
// Uploads & Users
|
||||
case 'clear-selection':
|
||||
return page.clearSelection()
|
||||
case 'select':
|
||||
return page.select(element, event)
|
||||
case 'select-all':
|
||||
return page.selectAll(element)
|
||||
case 'page-ellipsis':
|
||||
return page.focusJumpToPage()
|
||||
case 'page-prev':
|
||||
@ -429,6 +441,11 @@ page.getUploads = (params = {}) => {
|
||||
filters: params.filters || ''
|
||||
}
|
||||
|
||||
// Send client timezone offset if using date filter
|
||||
// Server will pretend client is on UTC if missing
|
||||
if (headers.filters.includes('date:') || headers.filters.includes('expiry:'))
|
||||
headers.minOffset = new Date().getTimezoneOffset()
|
||||
|
||||
axios.get(url, { headers }).then(response => {
|
||||
if (response.data.success === false)
|
||||
if (response.data.description === 'No token provided') {
|
||||
@ -464,10 +481,10 @@ page.getUploads = (params = {}) => {
|
||||
<form class="prevent-default">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input id="filters" class="input is-small" type="text" placeholder="Filters" value="${params.filters || ''}">
|
||||
<input id="filters" class="input is-small" type="text" placeholder="Filters" value="${page.escape(params.filters || '')}">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="user-filters-help">
|
||||
<button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="upload-filters-help">
|
||||
<span class="icon">
|
||||
<i class="icon-help-circled"></i>
|
||||
</span>
|
||||
@ -666,7 +683,7 @@ page.getUploads = (params = {}) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
|
||||
<th>File</th>
|
||||
<th>File name</th>
|
||||
${params.album === undefined ? `<th>${params.all ? 'User' : 'Album'}</th>` : ''}
|
||||
<th>Size</th>
|
||||
${params.all ? '<th>IP</th>' : ''}
|
||||
@ -946,40 +963,68 @@ page.clearSelection = () => {
|
||||
})
|
||||
}
|
||||
|
||||
page.userFiltersHelp = element => {
|
||||
page.uploadFiltersHelp = element => {
|
||||
const content = document.createElement('div')
|
||||
content.style = 'text-align: left'
|
||||
content.innerHTML = `
|
||||
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (upload name).
|
||||
Each key can be specified more than once.
|
||||
Backslashes should be used if the username contain whitespaces.
|
||||
There are 2 filter keys, namely <b>user</b> (username) and <b>ip</b>.
|
||||
These keys can be specified more than once.
|
||||
For usernames with whitespaces, wrap them with double quotes (<b>"</b>).
|
||||
|
||||
There are 2 special flags, namely <b>-user</b> and <b>-ip</b>, which will match uploads by non-registered users and have no IPs respectively.
|
||||
There are 2 special filter keys, namely <b>user:-</b> and <b>ip:-</b>, to match uploads by non-registered users and have no IPs respectively.
|
||||
|
||||
Matches can also be sorted with <b>orderby:columnName[:direction]</b> key.
|
||||
This key requires using the internal column names used in the database (size, timestamp, expirydate, and so on).
|
||||
To exclude certain users/ips while still listing every other uploads, add negation sign (<b>-</b>) before the keys.
|
||||
<b>Unfortunately</b>, this will not show uploads by non-registered users or have no IPs as well, instead you need to also use the special filter keys if you want to include them.
|
||||
|
||||
There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date).
|
||||
Their format is: <b>YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS</b> ("to" date is optional).
|
||||
If any of the subsequent date or time units are not specified, their first value will be used (e.g. January for month, 1 for day, and so on).
|
||||
If only time is specified, today's date will be used.
|
||||
Meaning the following can be accepted: <b>2020/01/01 01:23</b>, <b>2018/01/01 06</b>, <b>2019/11</b>, <b>12:34:56</b>.
|
||||
These keys can only be specified once each.
|
||||
|
||||
Matches can also be sorted with <b>orderby:columnName[:d[escending]]</b> keys.
|
||||
This key require internal column names used in the database (id, userid, and so on), but there are 2 shortcuts, namely <b>date</b> for timestamp column and <b>expiry</b> for expirydate column.
|
||||
This key can also be specified more than once, where their order will decide the sorting steps.
|
||||
|
||||
Any leftover keywords which do not use keys will be matched against the matches' file names.
|
||||
Excluding certain keywords is also supported by adding negation sign (<b>-</b>) before the keywords.
|
||||
|
||||
<b>Internals:</b>
|
||||
First, it will filter uploads matching ANY of the supplied filter keys AND/OR special flags, if any.
|
||||
Second, it will refine the matches using the supplied <b>name</b> keys, if any.
|
||||
Third, it will sort the matches using the supplied <b>orderby</b> keys, if any.
|
||||
First, it will filter uploads matching ANY of the supplied filter keys AND/OR special filter keys, if any.
|
||||
Second, it will refine the matches using the supplied <b>date</b> AND/OR <b>expiry</b> range keys, if any.
|
||||
Third, it will refine the matches using the leftover non-keyed keywords, if any.
|
||||
Finally, it will sort the matches using the supplied <b>orderby</b> keys, if any.
|
||||
|
||||
<b>Examples:</b>
|
||||
Uploads from user with username "demo":
|
||||
Uploads from user named "demo":
|
||||
<code>user:demo</code>
|
||||
Uploads from users with username "John Doe" AND/OR "demo":
|
||||
<code>user:John\\ Doe user:demo</code>
|
||||
Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip":
|
||||
<code>ip:127.0.0.1 name:*.rar name:*.zip</code>
|
||||
Uploads from user with username "test" AND/OR from non-registered users:
|
||||
<code>user:test -user</code>
|
||||
Sort results by "size" column in ascending and descending order respectively:
|
||||
<code>orderby:expirydate</code>
|
||||
<code>user:demo orderby:size</code>
|
||||
<code>-user name:*.mp4 orderby:size:desc</code>
|
||||
Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
|
||||
<code>user:demo user:"John Doe" user:-</code>
|
||||
ALL uploads, including from non-registered users, but NOT the ones from user named "demo":
|
||||
<code>-user:demo user:-</code>
|
||||
Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
|
||||
<code>ip:127.0.0.1 *.rar *.zip</code>
|
||||
Uploads uploaded since "1 June 2019 00:00:00":
|
||||
<code>date:2019/06</code>
|
||||
Uploads uploaded between "7 April 2020 00:00:00" and "7 April 2020 23:59:59":
|
||||
<code>date:2020/04/07-2020/04/07 23:59:59</code>
|
||||
Uploads which file names match "*.gz" but NOT "*.tar.gz":
|
||||
<code>*.gz -*.tar.gz</code>
|
||||
Sort matches by "size" column in ascending and descending order respectively:
|
||||
<code>user:"John Doe" orderby:size</code>
|
||||
<code>*.mp4 user:- orderby:size:d</code>
|
||||
|
||||
<b>Friendly reminder:</b> This window can be scrolled up!
|
||||
`.trim().replace(/^ {6}/gm, '').replace(/\n/g, '<br>')
|
||||
swal({ content })
|
||||
|
||||
swal({ content }).then(() => {
|
||||
// Restore modal size
|
||||
document.body.querySelector('.swal-overlay .swal-modal').classList.remove('is-expanded')
|
||||
})
|
||||
|
||||
// Expand modal size
|
||||
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
|
||||
}
|
||||
|
||||
page.filterUploads = element => {
|
||||
@ -991,9 +1036,13 @@ page.viewUserUploads = (id, element) => {
|
||||
const user = page.cache.users[id]
|
||||
if (!user) return
|
||||
element.classList.add('is-loading')
|
||||
// Wrap username in quotes if it contains whitespaces
|
||||
const username = user.username.includes(' ')
|
||||
? `"${user.username}"`
|
||||
: user.username
|
||||
page.getUploads({
|
||||
all: true,
|
||||
filters: `user:${user.username.replace(/ /g, '\\ ')}`,
|
||||
filters: `user:${username}`,
|
||||
trigger: document.querySelector('#itemManageUploads')
|
||||
})
|
||||
}
|
||||
@ -1682,7 +1731,9 @@ page.changeToken = (params = {}) => {
|
||||
swal({
|
||||
title: 'Woohoo!',
|
||||
text: 'Your token was successfully changed.',
|
||||
icon: 'success'
|
||||
icon: 'success',
|
||||
buttons: false,
|
||||
timer: 1500
|
||||
}).then(() => {
|
||||
axios.defaults.headers.common.token = response.data.token
|
||||
localStorage[lsKeys.token] = response.data.token
|
||||
@ -1806,10 +1857,10 @@ page.getUsers = (params = {}) => {
|
||||
<form class="prevent-default">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input id="filters" class="input is-small" type="text" placeholder="Filters (WIP)" value="${params.filters || ''}" disabled>
|
||||
<input id="filters" class="input is-small" type="text" placeholder="Filters (WIP)" value="${page.escape(params.filters || '')}" disabled>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="upload-filters-help" disabled>
|
||||
<button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="user-filters-help" disabled>
|
||||
<span class="icon">
|
||||
<i class="icon-help-circled"></i>
|
||||
</span>
|
||||
@ -1851,11 +1902,11 @@ page.getUsers = (params = {}) => {
|
||||
const controls = `
|
||||
<div class="columns">
|
||||
<div class="column has-text-left">
|
||||
<a class="button is-small is-primary is-outlined" title="Create user (WIP)" data-action="create-user">
|
||||
<a class="button is-small is-primary is-outlined" title="Create new user" data-action="create-user">
|
||||
<span class="icon">
|
||||
<i class="icon-plus"></i>
|
||||
</span>
|
||||
<span>Create user</span>
|
||||
<span>Create new user</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
@ -1891,7 +1942,6 @@ page.getUsers = (params = {}) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Uploads</th>
|
||||
<th>Usage</th>
|
||||
@ -1933,7 +1983,6 @@ page.getUsers = (params = {}) => {
|
||||
tr.dataset.id = user.id
|
||||
tr.innerHTML = `
|
||||
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td>
|
||||
<th>${user.id}</th>
|
||||
<th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td>
|
||||
<th>${user.uploads}</th>
|
||||
<td>${page.getPrettyBytes(user.usage)}</td>
|
||||
@ -2133,26 +2182,37 @@ page.editUser = id => {
|
||||
return swal('An error occurred!', response.data.description, 'error')
|
||||
}
|
||||
|
||||
if (response.data.password) {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `
|
||||
<p><b>${user.username}</b>'s new password is:</p>
|
||||
<p><code>${response.data.password}</code></p>
|
||||
`
|
||||
swal({
|
||||
title: 'Success!',
|
||||
icon: 'success',
|
||||
content: div
|
||||
})
|
||||
} else if (response.data.update && response.data.update.username !== user.username) {
|
||||
swal('Success!', `${user.username} was renamed into: ${response.data.update.username}.`, 'success')
|
||||
} else {
|
||||
swal('Success!', 'The user was edited!', 'success', {
|
||||
buttons: false,
|
||||
timer: 1500
|
||||
})
|
||||
let autoClose = true
|
||||
const div = document.createElement('div')
|
||||
|
||||
let displayName = user.username
|
||||
if (response.data.update.username !== user.username) {
|
||||
div.innerHTML += `<p>${user.username} was renamed into: <b>${response.data.update.username}</b>.</p>`
|
||||
autoClose = false
|
||||
displayName = response.data.update.username
|
||||
}
|
||||
|
||||
if (response.data.update.password) {
|
||||
div.innerHTML += `
|
||||
<p>${displayName}'s new password is:</p>
|
||||
<p><code>${response.data.update.password}</code></p>
|
||||
`
|
||||
autoClose = false
|
||||
}
|
||||
|
||||
if (response.data.update.enabled !== user.enabled)
|
||||
div.innerHTML += `<p>${displayName} has been ${response.data.update.enabled ? 'enabled' : 'disabled'}!</p>`
|
||||
|
||||
if (!div.innerHTML)
|
||||
div.innerHTML = `<p>${displayName} was edited!</p>`
|
||||
|
||||
swal({
|
||||
title: 'Success!',
|
||||
icon: 'success',
|
||||
content: div,
|
||||
buttons: !autoClose,
|
||||
timer: autoClose ? 1500 : null
|
||||
})
|
||||
page.getUsers(page.views.users)
|
||||
}).catch(page.onAxiosError)
|
||||
})
|
||||
|
@ -43,9 +43,9 @@ ${headers.join(',\n')}
|
||||
}
|
||||
|
||||
page.getPrettyDate = date => {
|
||||
return date.getFullYear() + '-' +
|
||||
return date.getFullYear() + '/' +
|
||||
(date.getMonth() < 9 ? '0' : '') + // month's index starts from zero
|
||||
(date.getMonth() + 1) + '-' +
|
||||
(date.getMonth() + 1) + '/' +
|
||||
(date.getDate() < 10 ? '0' : '') +
|
||||
date.getDate() + ' ' +
|
||||
(date.getHours() < 10 ? '0' : '') +
|
||||
@ -72,3 +72,56 @@ page.getPrettyBytes = num => {
|
||||
const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')
|
||||
return `${neg}${numStr} ${pre}B`
|
||||
}
|
||||
|
||||
page.escape = string => {
|
||||
// MIT License
|
||||
// Copyright(c) 2012-2013 TJ Holowaychuk
|
||||
// Copyright(c) 2015 Andreas Lubbe
|
||||
// Copyright(c) 2015 Tiancheng "Timothy" Gu
|
||||
|
||||
if (!string)
|
||||
return string
|
||||
|
||||
const str = String(string)
|
||||
const match = /["'&<>]/.exec(str)
|
||||
|
||||
if (!match)
|
||||
return str
|
||||
|
||||
let escape
|
||||
let html = ''
|
||||
let index = 0
|
||||
let lastIndex = 0
|
||||
|
||||
for (index = match.index; index < str.length; index++) {
|
||||
switch (str.charCodeAt(index)) {
|
||||
case 34: // "
|
||||
escape = '"'
|
||||
break
|
||||
case 38: // &
|
||||
escape = '&'
|
||||
break
|
||||
case 39: // '
|
||||
escape = '''
|
||||
break
|
||||
case 60: // <
|
||||
escape = '<'
|
||||
break
|
||||
case 62: // >
|
||||
escape = '>'
|
||||
break
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if (lastIndex !== index)
|
||||
html += str.substring(lastIndex, index)
|
||||
|
||||
lastIndex = index + 1
|
||||
html += escape
|
||||
}
|
||||
|
||||
return lastIndex !== index
|
||||
? html + str.substring(lastIndex, index)
|
||||
: html
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"1": "1587108179",
|
||||
"1": "1587239207",
|
||||
"2": "1581416390",
|
||||
"3": "1581416390",
|
||||
"4": "1581416390",
|
||||
|
93
yarn.lock
93
yarn.lock
@ -144,7 +144,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
|
||||
integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
|
||||
|
||||
"@babel/runtime@^7.7.7", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.7.7", "@babel/runtime@^7.9.2":
|
||||
version "7.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
|
||||
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
|
||||
@ -286,9 +286,9 @@
|
||||
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
|
||||
|
||||
"@types/node@*":
|
||||
version "13.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
|
||||
integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
|
||||
version "13.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.0.tgz#30d2d09f623fe32cde9cb582c7a6eda2788ce4a8"
|
||||
integrity sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
@ -930,9 +930,9 @@ buffer-from@^1.0.0:
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
|
||||
buffer@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce"
|
||||
integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
|
||||
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
|
||||
dependencies:
|
||||
base64-js "^1.0.2"
|
||||
ieee754 "^1.1.4"
|
||||
@ -1037,14 +1037,14 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-db@^1.0.30001017:
|
||||
version "1.0.30001040"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001040.tgz#dd0f810b5d078175e6bbddc860eb4e7917885362"
|
||||
integrity sha512-rrwOYbMn4sTFNdqT4sD1gtrfOKr8CPyUEw3FMhlEYb4b+KHhBJY+a5NROsNTBDoABVHyW6v1EKG1iuJx+gosAg==
|
||||
version "1.0.30001042"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001042.tgz#ac3e6065c7c46fc0bdb0fb1a13d861af8399c3a4"
|
||||
integrity sha512-2RKrB2hkLCW/8Uj32oaXj0O+N9ROo0/BF0EueWHwgs6AeeSiL+rCSsbICR3ayBJOZavgcFx65ZCw7QiafsoUFQ==
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001038, caniuse-lite@^1.0.30001039:
|
||||
version "1.0.30001040"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz#103fc8e6eb1d7397e95134cd0e996743353d58ea"
|
||||
integrity sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ==
|
||||
version "1.0.30001042"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001042.tgz#c91ec21ec2d270bd76dbc2ce261260c292b8c93c"
|
||||
integrity sha512-igMQ4dlqnf4tWv0xjaaE02op9AJ2oQzXKjWf4EuAHFN694Uo9/EfPVIPJcmn2WkU9RqozCxx5e2KPcVClHDbDw==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
@ -1183,9 +1183,9 @@ cli-cursor@^3.1.0:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
|
||||
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
|
||||
|
||||
cliui@^3.2.0:
|
||||
version "3.2.0"
|
||||
@ -1973,9 +1973,9 @@ ee-first@1.1.1:
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
electron-to-chromium@^1.3.390:
|
||||
version "1.3.403"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.403.tgz#c8bab4e2e72bf78bc28bad1cc355c061f9cc1918"
|
||||
integrity sha512-JaoxV4RzdBAZOnsF4dAlZ2ijJW72MbqO5lNfOBHUWiBQl3Rwe+mk2RCUMrRI3rSClLJ8HSNQNqcry12H+0ZjFw==
|
||||
version "1.3.413"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.413.tgz#9c457a4165c7b42e59d66dff841063eb9bfe5614"
|
||||
integrity sha512-Jm1Rrd3siqYHO3jftZwDljL2LYQafj3Kki5r+udqE58d0i91SkjItVJ5RwlJn9yko8i7MOcoidVKjQlgSdd1hg==
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
@ -2265,11 +2265,11 @@ esprima@^4.0.0:
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.0.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe"
|
||||
integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
|
||||
integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
|
||||
dependencies:
|
||||
estraverse "^5.0.0"
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrecurse@^4.1.0:
|
||||
version "4.2.1"
|
||||
@ -2283,10 +2283,10 @@ estraverse@^4.1.0, estraverse@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22"
|
||||
integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==
|
||||
estraverse@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
|
||||
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
@ -4040,10 +4040,10 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
|
||||
knex@^0.20.13:
|
||||
version "0.20.13"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.13.tgz#056c310d963f7efce1b3c7397576add1323f1146"
|
||||
integrity sha512-YVl//Te0G5suc+d9KyeI6WuhtgVlxu6HXYQB+WqrccFkSZAbHqlqZlUMogYG3UoVq69c3kiFbbxgUNkrO0PVfg==
|
||||
knex@^0.20.15:
|
||||
version "0.20.15"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.15.tgz#b7e9e1efd9cf35d214440d9439ed21153574679d"
|
||||
integrity sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q==
|
||||
dependencies:
|
||||
colorette "1.1.0"
|
||||
commander "^4.1.1"
|
||||
@ -6419,9 +6419,9 @@ resolve-url@^0.2.1:
|
||||
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
||||
|
||||
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.4.0:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
|
||||
integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c"
|
||||
integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==
|
||||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
@ -6527,6 +6527,11 @@ sax@^1.2.4, sax@~1.2.4:
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
search-query-parser@^1.5.5:
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.5.5.tgz#dfd829f4fb567a9a3a5b70327576dff4d5be071d"
|
||||
integrity sha512-8hKmRbHShAc+LJtUu7mu4L/oWVgICG1GCzWZtlR11Vl3T/v4aTGv+Ie2bPg+8ueJn+l8zMHCQhzrBNMoV/t2qw==
|
||||
|
||||
semver-diff@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
|
||||
@ -6552,9 +6557,9 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.1.3:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.2.tgz#d01432d74ed3010a20ffaf909d63a691520521cd"
|
||||
integrity sha512-Zo84u6o2PebMSK3zjJ6Zp5wi8VnQZnEaCP13Ul/lt1ANsLACxnJxq4EEm1PY94/por1Hm9+7xpIswdS5AkieMA==
|
||||
version "7.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||
|
||||
send@0.17.1:
|
||||
version "0.17.1"
|
||||
@ -7937,16 +7942,16 @@ yallist@^4.0.0:
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.7.2:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a"
|
||||
integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.1.tgz#2df608ca571a0cf94e25e417e2795c08f48acdc5"
|
||||
integrity sha512-xbWX1ayUVoW8DPM8qxOBowac4XxSTi0mFLbiokRq880ViYglN+F3nJ4Dc2GdypXpykrknKS39d8I3lzFoHv1kA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
yargs-parser@^18.1.1:
|
||||
version "18.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"
|
||||
integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==
|
||||
version "18.1.3"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user