Added delete user feature.
API: /api/users/delete
json: id<number>, purge[boolean]
By default will not purge out files, but will still clear userid
attribute from the files.
All associated albums will also be marked, and have their ZIP archives
be unliked, if applicable.

Fixed purging albums not properly reporting amount of associated files
that could not be removed, if any.

Fixed moderators being able to disable users by manually sending API
requests, if they at least know of the user IDs.
They could only disable regular users however.
This commit is contained in:
Bobby Wibowo 2019-10-07 06:11:07 +07:00
parent 5e60b01fe6
commit 4f04225ba0
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
11 changed files with 239 additions and 80 deletions

View File

@ -183,6 +183,7 @@ self.delete = async (req, res, next) => {
if (failed.length)
return res.json({ success: false, failed })
}
utils.invalidateStatsCache('uploads')
}
await db.table('albums')
@ -245,53 +246,48 @@ self.edit = async (req, res, next) => {
// Old rename API
return res.json({ success: false, description: 'You did not specify a new name.' })
await db.table('albums')
.where({
id,
userid: user.id
})
.update({
name,
editedAt: Math.floor(Date.now() / 1000),
download: Boolean(req.body.download),
public: Boolean(req.body.public),
description: typeof req.body.description === 'string'
? utils.escape(req.body.description.trim().substring(0, self.descMaxLength))
: ''
})
utils.invalidateAlbumsCache([id])
if (!req.body.requestLink)
return res.json({ success: true, name })
const oldIdentifier = album.identifier
const newIdentifier = await self.getUniqueRandomName()
await db.table('albums')
.where({
id,
userid: user.id
})
.update('identifier', newIdentifier)
utils.invalidateStatsCache('albums')
self.onHold.delete(newIdentifier)
// Rename zip archive of the album if it exists
try {
const oldZip = path.join(paths.zips, `${oldIdentifier}.zip`)
// await paths.access(oldZip)
const newZip = path.join(paths.zips, `${newIdentifier}.zip`)
await paths.rename(oldZip, newZip)
} catch (err) {
// Re-throw error
if (err.code !== 'ENOENT')
throw err
const update = {
name,
editedAt: Math.floor(Date.now() / 1000),
download: Boolean(req.body.download),
public: Boolean(req.body.public),
description: typeof req.body.description === 'string'
? utils.escape(req.body.description.trim().substring(0, self.descMaxLength))
: ''
}
return res.json({
success: true,
identifier: newIdentifier
})
if (req.body.requestLink)
update.identifier = await self.getUniqueRandomName()
await db.table('albums')
.where({
id,
userid: user.id
})
.update(update)
utils.invalidateAlbumsCache([id])
if (req.body.requestLink) {
self.onHold.delete(update.identifier)
// Rename zip archive of the album if it exists
try {
const oldZip = path.join(paths.zips, `${album.identifier}.zip`)
const newZip = path.join(paths.zips, `${update.identifier}.zip`)
await paths.rename(oldZip, newZip)
} catch (err) {
// Re-throw error
if (err.code !== 'ENOENT')
throw err
}
return res.json({
success: true,
identifier: update.identifier
})
} else {
return res.json({ success: true, name })
}
} catch (error) {
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })

View File

@ -1,5 +1,7 @@
const bcrypt = require('bcrypt')
const path = require('path')
const randomstring = require('randomstring')
const paths = require('./pathsController')
const perms = require('./permissionController')
const tokens = require('./tokenController')
const utils = require('./utilsController')
@ -135,10 +137,23 @@ self.changePassword = async (req, res, next) => {
}
}
self.assertPermission = (user, target) => {
if (!target)
throw new Error('Could not get user with the specified ID.')
else if (!perms.higher(user, target))
throw new Error('The user is in the same or higher group as you.')
else if (target.username === 'root')
throw new Error('Root user may not be tampered with.')
}
self.editUser = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const isadmin = perms.is(user, 'admin')
if (!isadmin)
return res.status(403).end()
const id = parseInt(req.body.id)
if (isNaN(id))
return res.json({ success: false, description: 'No user specified.' })
@ -147,23 +162,14 @@ self.editUser = async (req, res, next) => {
const target = await db.table('users')
.where('id', id)
.first()
if (!target)
return res.json({ success: false, description: 'Could not get user with the specified ID.' })
else if (!perms.higher(user, target))
return res.json({ success: false, description: 'The user is in the same or higher group as you.' })
else if (target.username === 'root')
return res.json({ success: false, description: 'Root user may not be edited.' })
self.assertPermission(user, target)
const update = {}
if (req.body.username !== undefined) {
update.username = String(req.body.username).trim()
if (update.username.length < self.user.min || update.username.length > self.user.max)
return res.json({
success: false,
description: `Username must have ${self.user.min}-${self.user.max} characters.`
})
throw new Error(`Username must have ${self.user.min}-${self.user.max} characters.`)
}
if (req.body.enabled !== undefined)
@ -191,7 +197,10 @@ self.editUser = async (req, res, next) => {
return res.json(response)
} catch (error) {
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
return res.status(500).json({
success: false,
description: error.message || 'An unexpected error occurred. Try again?'
})
}
}
@ -200,6 +209,86 @@ self.disableUser = async (req, res, next) => {
return self.editUser(req, res, next)
}
self.deleteUser = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const isadmin = perms.is(user, 'admin')
if (!isadmin)
return res.status(403).end()
const id = parseInt(req.body.id)
const purge = req.body.purge
if (isNaN(id))
return res.json({ success: false, description: 'No user specified.' })
try {
const target = await db.table('users')
.where('id', id)
.first()
self.assertPermission(user, target)
const files = await db.table('files')
.where('userid', id)
.select('id')
if (files.length) {
const fileids = files.map(file => file.id)
if (purge) {
const failed = await utils.bulkDeleteFromDb('id', fileids, user)
if (failed.length)
return res.json({ success: false, failed })
utils.invalidateStatsCache('uploads')
} else {
// Clear out userid attribute from the files
await db.table('files')
.whereIn('id', fileids)
.update('userid', null)
}
}
// TODO: Figure out obstacles of just deleting the albums
const albums = await db.table('albums')
.where('userid', id)
.where('enabled', 1)
.select('id', 'identifier')
if (albums.length) {
const albumids = albums.map(album => album.id)
await db.table('albums')
.whereIn('id', albumids)
.del()
utils.invalidateAlbumsCache(albumids)
// Unlink their archives
await Promise.all(albums.map(async album => {
try {
await paths.unlink(path.join(paths.zips, `${album.identifier}.zip`))
} catch (error) {
if (error.code !== 'ENOENT')
throw error
}
}))
}
await db.table('users')
.where('id', id)
.del()
utils.invalidateStatsCache('users')
return res.json({ success: true })
} catch (error) {
return res.status(500).json({
success: false,
description: error.message || 'An unexpected error occurred. Try again?'
})
}
}
self.bulkDeleteUsers = async (req, res, next) => {
// TODO
}
self.listUsers = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return

View File

@ -1,2 +1,2 @@
body{-webkit-animation:none;animation:none}#dashboard{-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}.section{background:none}.menu-list a{color:#209cee;border:1px solid transparent;margin-top:-1px}.menu-list a.is-active{color:#fff;background:#209cee;border-color:#209cee}.menu-list a:not(.is-active):hover{color:#209cee;background:none;border-color:#209cee}.menu-list a[disabled]{color:#7a7a7a;pointer-events:none}.menu-list a.is-loading:after{-webkit-animation:spinAround .5s linear infinite;animation:spinAround .5s linear infinite;border-radius:290486px;border-color:transparent transparent #dbdbdb #dbdbdb;border-style:solid;border-width:2px;content:"";display:block;height:1em;width:1em;right:.5em;top:calc(50% - .5em);position:absolute!important}ul#albumsContainer{border-left:0;padding-left:0}ul#albumsContainer li{border-left:2px solid #585858;padding-left:.75em}#page.fade-in,ul#albumsContainer li{-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}.pagination{margin-bottom:1.25rem}.pagination a:not([disabled]){color:#eff0f1;border-color:#eff0f1;background:none}.pagination a.pagination-link:hover,.pagination a.pagination-next:not([disabled]):hover,.pagination a.pagination-previous:not([disabled]):hover{color:#000;background-color:#eff0f1;border-color:#eff0f1}.pagination a.pagination-link.is-current{color:#000;background-color:#eff0f1}.pagination a.is-loading:hover:after,.pagination a.pagination-link.is-current.is-loading:after{border-bottom-color:#000;border-left-color:#000}li[data-action=page-ellipsis]{cursor:pointer}.label{color:#bdc3c7}.menu-list li ul{border-left-color:#898b8d}.image-container .checkbox{position:absolute;top:11px;left:11px}.image-container .controls{display:flex;position:absolute;top:11px;right:11px}.image-container .controls .button{border-radius:0}.image-container .controls .button:not(:active):not(:hover){color:#fff;background-color:rgba(0,0,0,.56078)}.no-touch .image-container .checkbox{opacity:.5}.no-touch .image-container .controls,.no-touch .image-container .details{opacity:0}.no-touch .image-container:hover .checkbox,.no-touch .image-container:hover .controls,.no-touch .image-container:hover .details{opacity:1}#page{min-width:0}.table{color:#bdc3c7;background-color:#000;font-size:.75rem}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#2f2f2f}.table td,.table th{white-space:nowrap;vertical-align:middle;border-bottom:1px solid #585858}.table th{color:#eff0f1;height:2.25em;font-weight:400}.table thead td,.table thead th{color:#eff0f1;background-color:#585858;border-bottom:0;height:33px}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:1px}.table .cell-indent{padding-left:2.25em}.is-linethrough{text-decoration:line-through}#menu.is-loading .menu-list a{cursor:progress}#statistics tr :first-child{width:50%}.expirydate{color:#bdc3c7}
body{-webkit-animation:none;animation:none}#dashboard{-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}.section{background:none}.menu-list a{color:#209cee;border:1px solid transparent;margin-top:-1px}.menu-list a.is-active{color:#fff;background:#209cee;border-color:#209cee}.menu-list a:not(.is-active):hover{color:#209cee;background:none;border-color:#209cee}.menu-list a[disabled]{color:#7a7a7a;pointer-events:none}.menu-list a.is-loading:after{-webkit-animation:spinAround .5s linear infinite;animation:spinAround .5s linear infinite;border-radius:290486px;border-color:transparent transparent #dbdbdb #dbdbdb;border-style:solid;border-width:2px;content:"";display:block;height:1em;width:1em;right:.5em;top:calc(50% - .5em);position:absolute!important}ul#albumsContainer{border-left:0;padding-left:0}ul#albumsContainer li{border-left:2px solid #585858;padding-left:.75em}#page.fade-in,ul#albumsContainer li{-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}.pagination{margin-bottom:1.25rem}.pagination a:not([disabled]){color:#eff0f1;border-color:#eff0f1;background:none}.pagination a.pagination-link:hover,.pagination a.pagination-next:not([disabled]):hover,.pagination a.pagination-previous:not([disabled]):hover{color:#000;background-color:#eff0f1;border-color:#eff0f1}.pagination a.pagination-link.is-current{color:#000;background-color:#eff0f1}.pagination a.is-loading:hover:after,.pagination a.pagination-link.is-current.is-loading:after{border-bottom-color:#000;border-left-color:#000}li[data-action=page-ellipsis]{cursor:pointer}.label{color:#bdc3c7}.menu-list li ul{border-left-color:#898b8d}.image-container .checkbox{position:absolute;top:11px;left:11px}.image-container .controls{display:flex;position:absolute;top:11px;right:11px}.image-container .controls .button{border-radius:0}.image-container .controls .button:not(:active):not(:hover){color:#fff;background-color:rgba(0,0,0,.56078)}.no-touch .image-container .checkbox{opacity:.5}.no-touch .image-container .controls,.no-touch .image-container .details{opacity:0}.no-touch .image-container:hover .checkbox,.no-touch .image-container:hover .controls,.no-touch .image-container:hover .details{opacity:1}#page{min-width:0}.table{color:#bdc3c7;background-color:#000;font-size:.75rem}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#2f2f2f}.table td,.table th{white-space:nowrap;vertical-align:middle;border-bottom:1px solid #585858}.table th{color:#eff0f1;height:2.25em;font-weight:400}.table thead td,.table thead th{color:#eff0f1;background-color:#585858;border-bottom:0;height:31px}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:1px}.table .cell-indent{padding-left:2.25em}.is-linethrough{text-decoration:line-through}#menu.is-loading .menu-list a{cursor:progress}#statistics tr :first-child{width:50%}.expirydate{color:#bdc3c7}
/*# sourceMappingURL=dashboard.css.map */

View File

@ -1 +1 @@
{"version":3,"sources":["css/dashboard.css"],"names":[],"mappings":"AAAA,KACE,sBAAc,CAAd,cACF,CAEA,WACE,mCAA4B,CAA5B,2BACF,CAEA,SACE,eACF,CAEA,aACE,aAAc,CACd,4BAA6B,CAC7B,eACF,CAEA,uBACE,UAAW,CACX,kBAAmB,CACnB,oBACF,CAEA,mCACE,aAAc,CACd,eAAgB,CAChB,oBACF,CAEA,uBACE,aAAc,CACd,mBACF,CAEA,8BACE,gDAA0C,CAA1C,wCAA0C,CAE1C,sBAAuB,CAEvB,oDAA6B,CAA7B,kBAA6B,CAA7B,gBAA6B,CAC7B,UAAW,CACX,aAAc,CACd,UAAW,CACX,SAAU,CACV,UAA2B,CAC3B,oBAA0B,CAC1B,2BACF,CAEA,mBACE,aAAc,CACd,cACF,CAEA,sBACE,6BAA8B,CAC9B,kBAEF,CAEA,oCAHE,mCAA4B,CAA5B,2BAKF,CAEA,YACE,qBACF,CAEA,8BACE,aAAc,CACd,oBAAqB,CACrB,eACF,CAEA,gJAGE,UAAW,CACX,wBAAyB,CACzB,oBACF,CAEA,yCACE,UAAW,CACX,wBACF,CAEA,+FAEE,wBAAyB,CACzB,sBACF,CAEA,8BACE,cACF,CAEA,OACE,aACF,CAEA,iBACE,yBACF,CAEA,2BACE,iBAAkB,CAClB,QAAS,CACT,SACF,CAEA,2BACE,YAAa,CACb,iBAAkB,CAClB,QAAS,CACT,UACF,CAEA,mCACE,eACF,CAEA,4DACE,UAAW,CACX,mCACF,CAEA,qCACE,UACF,CAEA,yEAEE,SACF,CAEA,gIAGE,SACF,CAEA,MAEE,WACF,CAEA,OACE,aAAc,CACd,qBAAsB,CACtB,gBACF,CAEA,qDACE,wBACF,CAEA,oBAEE,kBAAmB,CACnB,qBAAsB,CACtB,+BACF,CAEA,UACE,aAAc,CACd,aAAc,CACd,eACF,CAEA,gCAEE,aAAc,CACd,wBAAyB,CACzB,eAAgB,CAChB,WACF,CAEA,4DAEE,uBACF,CAEA,oBACE,mBACF,CAEA,gBACE,4BACF,CAEA,8BACE,eACF,CAEA,4BACE,SACF,CAEA,YACE,aACF","file":"dashboard.css","sourcesContent":["body {\n animation: none\n}\n\n#dashboard {\n animation: fadeInOpacity 0.5s\n}\n\n.section {\n background: none\n}\n\n.menu-list a {\n color: #209cee;\n border: 1px solid transparent;\n margin-top: -1px\n}\n\n.menu-list a.is-active {\n color: #fff;\n background: #209cee;\n border-color: #209cee\n}\n\n.menu-list a:not(.is-active):hover {\n color: #209cee;\n background: none;\n border-color: #209cee\n}\n\n.menu-list a[disabled] {\n color: #7a7a7a;\n pointer-events: none\n}\n\n.menu-list a.is-loading::after {\n animation: spinAround 0.5s infinite linear;\n border: 2px solid #dbdbdb;\n border-radius: 290486px;\n border-right-color: transparent;\n border-top-color: transparent;\n content: \"\";\n display: block;\n height: 1em;\n width: 1em;\n right: calc(0% + (1em / 2));\n top: calc(50% - (1em / 2));\n position: absolute !important\n}\n\nul#albumsContainer {\n border-left: 0;\n padding-left: 0\n}\n\nul#albumsContainer li {\n border-left: 2px solid #585858;\n padding-left: 0.75em;\n animation: fadeInOpacity 0.5s\n}\n\n#page.fade-in {\n animation: fadeInOpacity 0.5s\n}\n\n.pagination {\n margin-bottom: 1.25rem\n}\n\n.pagination a:not([disabled]) {\n color: #eff0f1;\n border-color: #eff0f1;\n background: none\n}\n\n.pagination a.pagination-link:hover,\n.pagination a.pagination-next:not([disabled]):hover,\n.pagination a.pagination-previous:not([disabled]):hover {\n color: #000;\n background-color: #eff0f1;\n border-color: #eff0f1\n}\n\n.pagination a.pagination-link.is-current {\n color: #000;\n background-color: #eff0f1\n}\n\n.pagination a.is-loading:hover::after,\n.pagination a.pagination-link.is-current.is-loading::after {\n border-bottom-color: #000;\n border-left-color: #000\n}\n\nli[data-action=\"page-ellipsis\"] {\n cursor: pointer\n}\n\n.label {\n color: #bdc3c7\n}\n\n.menu-list li ul {\n border-left-color: #898b8d\n}\n\n.image-container .checkbox {\n position: absolute;\n top: 11px;\n left: 11px\n}\n\n.image-container .controls {\n display: flex;\n position: absolute;\n top: 11px;\n right: 11px\n}\n\n.image-container .controls .button {\n border-radius: 0\n}\n\n.image-container .controls .button:not(:active):not(:hover) {\n color: #fff;\n background-color: #0000008f\n}\n\n.no-touch .image-container .checkbox {\n opacity: 0.5\n}\n\n.no-touch .image-container .controls,\n.no-touch .image-container .details {\n opacity: 0\n}\n\n.no-touch .image-container:hover .checkbox,\n.no-touch .image-container:hover .controls,\n.no-touch .image-container:hover .details {\n opacity: 1\n}\n\n#page {\n /* fix overflow issue with flex */\n min-width: 0\n}\n\n.table {\n color: #bdc3c7;\n background-color: #000;\n font-size: 0.75rem\n}\n\n.table.is-hoverable tbody tr:not(.is-selected):hover {\n background-color: #2f2f2f\n}\n\n.table td,\n.table th {\n white-space: nowrap;\n vertical-align: middle;\n border-bottom: 1px solid #585858\n}\n\n.table th {\n color: #eff0f1;\n height: 2.25em;\n font-weight: normal\n}\n\n.table thead td,\n.table thead th {\n color: #eff0f1;\n background-color: #585858;\n border-bottom: 0;\n height: 33px\n}\n\n.table tbody tr:last-child td,\n.table tbody tr:last-child th {\n border-bottom-width: 1px\n}\n\n.table .cell-indent {\n padding-left: 2.25em\n}\n\n.is-linethrough {\n text-decoration: line-through\n}\n\n#menu.is-loading .menu-list a {\n cursor: progress\n}\n\n#statistics tr *:nth-child(1) {\n width: 50%\n}\n\n.expirydate {\n color: #bdc3c7\n}\n"]}
{"version":3,"sources":["css/dashboard.css"],"names":[],"mappings":"AAAA,KACE,sBAAc,CAAd,cACF,CAEA,WACE,mCAA4B,CAA5B,2BACF,CAEA,SACE,eACF,CAEA,aACE,aAAc,CACd,4BAA6B,CAC7B,eACF,CAEA,uBACE,UAAW,CACX,kBAAmB,CACnB,oBACF,CAEA,mCACE,aAAc,CACd,eAAgB,CAChB,oBACF,CAEA,uBACE,aAAc,CACd,mBACF,CAEA,8BACE,gDAA0C,CAA1C,wCAA0C,CAE1C,sBAAuB,CAEvB,oDAA6B,CAA7B,kBAA6B,CAA7B,gBAA6B,CAC7B,UAAW,CACX,aAAc,CACd,UAAW,CACX,SAAU,CACV,UAA2B,CAC3B,oBAA0B,CAC1B,2BACF,CAEA,mBACE,aAAc,CACd,cACF,CAEA,sBACE,6BAA8B,CAC9B,kBAEF,CAEA,oCAHE,mCAA4B,CAA5B,2BAKF,CAEA,YACE,qBACF,CAEA,8BACE,aAAc,CACd,oBAAqB,CACrB,eACF,CAEA,gJAGE,UAAW,CACX,wBAAyB,CACzB,oBACF,CAEA,yCACE,UAAW,CACX,wBACF,CAEA,+FAEE,wBAAyB,CACzB,sBACF,CAEA,8BACE,cACF,CAEA,OACE,aACF,CAEA,iBACE,yBACF,CAEA,2BACE,iBAAkB,CAClB,QAAS,CACT,SACF,CAEA,2BACE,YAAa,CACb,iBAAkB,CAClB,QAAS,CACT,UACF,CAEA,mCACE,eACF,CAEA,4DACE,UAAW,CACX,mCACF,CAEA,qCACE,UACF,CAEA,yEAEE,SACF,CAEA,gIAGE,SACF,CAEA,MAEE,WACF,CAEA,OACE,aAAc,CACd,qBAAsB,CACtB,gBACF,CAEA,qDACE,wBACF,CAEA,oBAEE,kBAAmB,CACnB,qBAAsB,CACtB,+BACF,CAEA,UACE,aAAc,CACd,aAAc,CACd,eACF,CAEA,gCAEE,aAAc,CACd,wBAAyB,CACzB,eAAgB,CAChB,WACF,CAEA,4DAEE,uBACF,CAEA,oBACE,mBACF,CAEA,gBACE,4BACF,CAEA,8BACE,eACF,CAEA,4BACE,SACF,CAEA,YACE,aACF","file":"dashboard.css","sourcesContent":["body {\n animation: none\n}\n\n#dashboard {\n animation: fadeInOpacity 0.5s\n}\n\n.section {\n background: none\n}\n\n.menu-list a {\n color: #209cee;\n border: 1px solid transparent;\n margin-top: -1px\n}\n\n.menu-list a.is-active {\n color: #fff;\n background: #209cee;\n border-color: #209cee\n}\n\n.menu-list a:not(.is-active):hover {\n color: #209cee;\n background: none;\n border-color: #209cee\n}\n\n.menu-list a[disabled] {\n color: #7a7a7a;\n pointer-events: none\n}\n\n.menu-list a.is-loading::after {\n animation: spinAround 0.5s infinite linear;\n border: 2px solid #dbdbdb;\n border-radius: 290486px;\n border-right-color: transparent;\n border-top-color: transparent;\n content: \"\";\n display: block;\n height: 1em;\n width: 1em;\n right: calc(0% + (1em / 2));\n top: calc(50% - (1em / 2));\n position: absolute !important\n}\n\nul#albumsContainer {\n border-left: 0;\n padding-left: 0\n}\n\nul#albumsContainer li {\n border-left: 2px solid #585858;\n padding-left: 0.75em;\n animation: fadeInOpacity 0.5s\n}\n\n#page.fade-in {\n animation: fadeInOpacity 0.5s\n}\n\n.pagination {\n margin-bottom: 1.25rem\n}\n\n.pagination a:not([disabled]) {\n color: #eff0f1;\n border-color: #eff0f1;\n background: none\n}\n\n.pagination a.pagination-link:hover,\n.pagination a.pagination-next:not([disabled]):hover,\n.pagination a.pagination-previous:not([disabled]):hover {\n color: #000;\n background-color: #eff0f1;\n border-color: #eff0f1\n}\n\n.pagination a.pagination-link.is-current {\n color: #000;\n background-color: #eff0f1\n}\n\n.pagination a.is-loading:hover::after,\n.pagination a.pagination-link.is-current.is-loading::after {\n border-bottom-color: #000;\n border-left-color: #000\n}\n\nli[data-action=\"page-ellipsis\"] {\n cursor: pointer\n}\n\n.label {\n color: #bdc3c7\n}\n\n.menu-list li ul {\n border-left-color: #898b8d\n}\n\n.image-container .checkbox {\n position: absolute;\n top: 11px;\n left: 11px\n}\n\n.image-container .controls {\n display: flex;\n position: absolute;\n top: 11px;\n right: 11px\n}\n\n.image-container .controls .button {\n border-radius: 0\n}\n\n.image-container .controls .button:not(:active):not(:hover) {\n color: #fff;\n background-color: #0000008f\n}\n\n.no-touch .image-container .checkbox {\n opacity: 0.5\n}\n\n.no-touch .image-container .controls,\n.no-touch .image-container .details {\n opacity: 0\n}\n\n.no-touch .image-container:hover .checkbox,\n.no-touch .image-container:hover .controls,\n.no-touch .image-container:hover .details {\n opacity: 1\n}\n\n#page {\n /* fix overflow issue with flex */\n min-width: 0\n}\n\n.table {\n color: #bdc3c7;\n background-color: #000;\n font-size: 0.75rem\n}\n\n.table.is-hoverable tbody tr:not(.is-selected):hover {\n background-color: #2f2f2f\n}\n\n.table td,\n.table th {\n white-space: nowrap;\n vertical-align: middle;\n border-bottom: 1px solid #585858\n}\n\n.table th {\n color: #eff0f1;\n height: 2.25em;\n font-weight: normal\n}\n\n.table thead td,\n.table thead th {\n color: #eff0f1;\n background-color: #585858;\n border-bottom: 0;\n height: 31px\n}\n\n.table tbody tr:last-child td,\n.table tbody tr:last-child th {\n border-bottom-width: 1px\n}\n\n.table .cell-indent {\n padding-left: 2.25em\n}\n\n.is-linethrough {\n text-decoration: line-through\n}\n\n#menu.is-loading .menu-list a {\n cursor: progress\n}\n\n#statistics tr *:nth-child(1) {\n width: 50%\n}\n\n.expirydate {\n color: #bdc3c7\n}\n"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,7 @@ routes.get('/users', (req, res, next) => authController.listUsers(req, res, next
routes.get('/users/:page', (req, res, next) => authController.listUsers(req, res, next))
routes.post('/users/edit', (req, res, next) => authController.editUser(req, res, next))
routes.post('/users/disable', (req, res, next) => authController.disableUser(req, res, next))
routes.post('/users/delete', (req, res, next) => authController.deleteUser(req, res, next))
routes.get('/stats', (req, res, next) => utilsController.stats(req, res, next))
module.exports = routes

View File

@ -174,7 +174,7 @@ li[data-action="page-ellipsis"] {
color: #eff0f1;
background-color: #585858;
border-bottom: 0;
height: 33px
height: 31px
}
.table tbody tr:last-child td,

View File

@ -329,6 +329,8 @@ page.domClick = event => {
return page.editUser(id)
case 'disable-user':
return page.disableUser(id)
case 'delete-user':
return page.deleteUser(id)
case 'filters-help':
return page.filtersHelp(element)
case 'filter-uploads':
@ -743,6 +745,7 @@ page.getUploads = (params = {}) => {
page.setUploadsView = (view, element) => {
localStorage[lsKeys.viewType[page.currentView]] = view
page.views[page.currentView].type = view
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign({
trigger: element
@ -964,7 +967,7 @@ page.filtersHelp = element => {
}
page.filterUploads = element => {
const filters = document.querySelector('#filters').value
const filters = document.querySelector('#filters').value.trim()
page.getUploads({ all: true, filters }, element)
}
@ -1516,14 +1519,18 @@ page.deleteAlbum = id => {
id,
purge: proceed === 'purge'
}).then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
if (response.data.success === false) {
const failed = Array.isArray(response.data.failed)
? response.data.failed
: []
if (response.data.description === 'No token provided')
return page.verifyToken(page.token)
} else if (Array.isArray(response.data.failed) && response.data.failed.length) {
return swal('An error occurred!', 'Unable to delete ', 'error')
} else {
else if (failed.length)
return swal('An error occurred!', `Unable to delete ${failed.length} of the album's upload${failed.length === 1 ? '' : 's'}.`, 'error')
else
return swal('An error occurred!', response.data.description, 'error')
}
}
swal('Deleted!', 'Your album has been deleted.', 'success')
page.getAlbumsSidebar()
@ -1746,11 +1753,14 @@ page.getUsers = (params = {}) => {
return swal('An error occurred!', response.data.description, 'error')
}
if (params.pageNum && (response.data.users.length === 0)) {
// Only remove loading class here, since beyond this the entire page will be replaced anyways
page.updateTrigger(params.trigger)
return swal('An error occurred!', `There are no more users to populate page ${params.pageNum + 1}.`, 'error')
}
if (params.pageNum && (response.data.users.length === 0))
if (params.autoPage) {
params.pageNum = Math.ceil(response.data.count / 25) - 1
return page.getUsers(params)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', `There are no more users to populate page ${params.pageNum + 1}.`, 'error')
}
page.currentView = 'users'
page.cache.users = {}
@ -1828,7 +1838,6 @@ page.getUsers = (params = {}) => {
</tbody>
</table>
</div>
<hr>
${pagination}
`
@ -1880,7 +1889,7 @@ page.getUsers = (params = {}) => {
<i class="icon-hammer"></i>
</span>
</a>
<a class="button is-small is-danger is-outlined is-hidden" title="Delete user (WIP)" data-action="delete-user" disabled>
<a class="button is-small is-danger is-outlined" title="Delete user" data-action="delete-user">
<span class="icon">
<i class="icon-trash"></i>
</span>
@ -2010,7 +2019,10 @@ page.disableUser = id => {
if (!user || !user.enabled) return
const content = document.createElement('div')
content.innerHTML = `You will be disabling a user with the username <b>${page.cache.users[id].username}</b>.`
content.innerHTML = `
<p>You will be disabling a user named <b>${page.cache.users[id].username}</b>.</p>
<p>Their files will remain.</p>
`
swal({
title: 'Are you sure?',
@ -2036,12 +2048,73 @@ page.disableUser = id => {
else
return swal('An error occurred!', response.data.description, 'error')
swal('Success!', 'The user has been disabled.', 'success')
swal('Success!', `${page.cache.users[id].username} has been disabled.`, 'success')
page.getUsers(page.views.users)
}).catch(page.onAxiosError)
})
}
page.deleteUser = id => {
const user = page.cache.users[id]
if (!user || !user.enabled) return
const content = document.createElement('div')
content.innerHTML = `
<p>You will be deleting a user named <b>${page.cache.users[id].username}</b>.<p>
<p>Their files will remain, unless you choose otherwise.</p>
`
swal({
title: 'Are you sure?',
icon: 'warning',
content,
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, delete it!',
closeModal: false
},
purge: {
text: 'Yes, and the uploads too!',
value: 'purge',
className: 'swal-button--danger',
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/users/delete', {
id,
purge: proceed === 'purge'
}).then(response => {
if (!response) return
if (response.data.success === false) {
const failed = Array.isArray(response.data.failed)
? response.data.failed
: []
if (response.data.description === 'No token provided')
return page.verifyToken(page.token)
else if (failed.length)
return swal('An error occurred!', `Unable to delete ${failed.length} of the user's upload${failed.length === 1 ? '' : 's'}.`, 'error')
else
return swal('An error occurred!', response.data.description, 'error')
}
swal('Success!', `${page.cache.users[id].username} has been deleted.`, 'success')
// Reload users list
// eslint-disable-next-line compat/compat
page.getUsers(Object.assign({
autoPage: true
}, page.views.users))
}).catch(page.onAxiosError)
})
}
// Roughly based on https://github.com/mayuska/pagination/blob/master/index.js
page.paginate = (totalItems, itemsPerPage, currentPage) => {
currentPage = currentPage + 1

View File

@ -1,5 +1,5 @@
{
"1": "1570315036",
"1": "1570403431",
"2": "1568894058",
"3": "1568894058",
"4": "1568894058",

View File

@ -28,7 +28,7 @@ Normal priority:
Low priority:
* [ ] Delete user feature.
* [x] Delete user feature.
* [ ] Bulk delete user feature.
* [ ] Bulk disable user feature.
* [ ] Strip EXIF from images. [#51](https://github.com/BobbyWibowo/lolisafe/issues/51)