From c3d4c237cbab49a4274b6617eee9f620bd21f056 Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Wed, 10 Oct 2018 02:52:41 +0700 Subject: [PATCH] Init account-manager branch --- controllers/albumsController.js | 7 +- controllers/authController.js | 142 +- controllers/tokenController.js | 32 +- controllers/uploadController.js | 23 +- controllers/utilsController.js | 13 +- database/db.js | 6 +- database/migration.js | 12 +- lolisafe.js | 13 +- public/css/dashboard.css | 24 +- public/css/sweetalert.css | 7 + public/js/.eslintrc.json | 13 +- public/js/album.js | 2 +- public/js/auth.js | 54 +- public/js/dashboard.js | 2114 ++++++++++++++++----------- public/js/home.js | 392 +++-- public/js/sharex.js | 37 +- public/libs/fontello/fontello.css | 29 +- public/libs/fontello/fontello.eot | Bin 11844 -> 12320 bytes public/libs/fontello/fontello.svg | 14 +- public/libs/fontello/fontello.ttf | Bin 11676 -> 12152 bytes public/libs/fontello/fontello.woff | Bin 7552 -> 7848 bytes public/libs/fontello/fontello.woff2 | Bin 6332 -> 6620 bytes routes/api.js | 3 + views/dashboard.njk | 9 + 24 files changed, 1762 insertions(+), 1184 deletions(-) diff --git a/controllers/albumsController.js b/controllers/albumsController.js index a0fa7f0..2a83b01 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -204,9 +204,12 @@ albumsController.edit = async (req, res, next) => { }) .first() - if (album && (album.id !== id)) { + if (!album) { + return res.json({ success: false, description: 'Could not get album with the specified ID.' }) + } else if (album.id !== id) { return res.json({ success: false, description: 'Name already in use.' }) } else if (req._old && (album.id === id)) { + // Old rename API return res.json({ success: false, description: 'You did not specify a new name.' }) } @@ -266,7 +269,7 @@ albumsController.rename = async (req, res, next) => { } albumsController.get = async (req, res, next) => { - // TODO: + // TODO: ... what was i supposed to do here? const identifier = req.params.identifier if (identifier === undefined) { return res.status(401).json({ success: false, description: 'No identifier provided.' }) diff --git a/controllers/authController.js b/controllers/authController.js index df553fc..d6ee913 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -6,6 +6,35 @@ const utils = require('./utilsController') const authController = {} +authController.permissions = { + user: 0, // upload & delete own files, create & delete albums + moderator: 50, // delete other user's files + admin: 80, // manage users (disable accounts) & create moderators + superadmin: 100 // create admins + // groups will inherit permissions from groups which have lower value +} + +authController.is = (user, group) => { + // root bypass + if (user.username === 'root') { return true } + const permission = user.permission || 0 + return permission >= authController.permissions[group] +} + +authController.higher = (user, target) => { + const userPermission = user.permission || 0 + const targetPermission = target.permission || 0 + return userPermission > targetPermission +} + +authController.mapPermissions = user => { + const map = {} + Object.keys(authController.permissions).forEach(group => { + map[group] = authController.is(user, group) + }) + return map +} + authController.verify = async (req, res, next) => { const username = req.body.username const password = req.body.password @@ -14,7 +43,9 @@ authController.verify = async (req, res, next) => { if (password === undefined) { return res.json({ success: false, description: 'No password provided.' }) } const user = await db.table('users').where('username', username).first() - if (!user) { return res.json({ success: false, description: 'Username doesn\'t exist.' }) } + if (!user) { + return res.json({ success: false, description: 'Username doesn\'t exist.' }) + } if (user.enabled === false || user.enabled === 0) { return res.json({ success: false, description: 'This account has been disabled.' }) } @@ -60,7 +91,8 @@ authController.register = async (req, res, next) => { username, password: hash, token, - enabled: 1 + enabled: 1, + permission: authController.permissions.user }) return res.json({ success: true, token }) }) @@ -94,23 +126,43 @@ authController.changePassword = async (req, res, next) => { authController.getFileLengthConfig = async (req, res, next) => { const user = await utils.authorize(req, res) if (!user) { return } - return res.json({ success: true, fileLength: user.fileLength, config: config.uploads.fileLength }) + return res.json({ + success: true, + fileLength: user.fileLength, + config: config.uploads.fileLength + }) } authController.changeFileLength = async (req, res, next) => { if (config.uploads.fileLength.userChangeable === false) { - return res.json({ success: false, description: 'Changing file name length is disabled at the moment.' }) + return res.json({ + success: false, + description: 'Changing file name length is disabled at the moment.' + }) } const user = await utils.authorize(req, res) if (!user) { return } const fileLength = parseInt(req.body.fileLength) - if (fileLength === undefined) { return res.json({ success: false, description: 'No file name length provided.' }) } - if (isNaN(fileLength)) { return res.json({ success: false, description: 'File name length is not a valid number.' }) } + if (fileLength === undefined) { + return res.json({ + success: false, + description: 'No file name length provided.' + }) + } + if (isNaN(fileLength)) { + return res.json({ + success: false, + description: 'File name length is not a valid number.' + }) + } if (fileLength < config.uploads.fileLength.min || fileLength > config.uploads.fileLength.max) { - return res.json({ success: false, description: `File name length must be ${config.uploads.fileLength.min} to ${config.uploads.fileLength.max} characters.` }) + return res.json({ + success: false, + description: `File name length must be ${config.uploads.fileLength.min} to ${config.uploads.fileLength.max} characters.` + }) } if (fileLength === user.fileLength) { @@ -124,4 +176,80 @@ authController.changeFileLength = async (req, res, next) => { return res.json({ success: true }) } +authController.editUser = async (req, res, next) => { + const user = await utils.authorize(req, res) + if (!user) { return } + + const id = parseInt(req.body.id) + if (isNaN(id)) { + return res.json({ success: false, description: 'No user specified.' }) + } + + 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 (!authController.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.' }) + } + + const username = String(req.body.username) + if (username.length < 4 || username.length > 32) { + return res.json({ success: false, description: 'Username must have 4-32 characters.' }) + } + + await db.table('users') + .where('id', id) + .update({ + username, + enabled: Boolean(req.body.enabled) + }) + + if (!req.body.resetPassword) { + return res.json({ success: true, username }) + } + + const password = randomstring.generate(16) + bcrypt.hash(password, 10, async (error, hash) => { + if (error) { + console.error(error) + return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻.' }) + } + + await db.table('users') + .where('id', id) + .update('password', hash) + + return res.json({ success: true, password }) + }) +} + +authController.listUsers = async (req, res, next) => { + const user = await utils.authorize(req, res) + if (!user) { return } + + const isadmin = authController.is(user, 'admin') + if (!isadmin) { return res.status(403) } + + let offset = req.params.page + if (offset === undefined) { offset = 0 } + + const users = await db.table('users') + // .orderBy('id', 'DESC') + .limit(25) + .offset(25 * offset) + .select('id', 'username', 'enabled', 'fileLength', 'permission') + + for (const user of users) { + user.groups = authController.mapPermissions(user) + delete user.permission + } + + return res.json({ success: true, users }) +} + module.exports = authController diff --git a/controllers/tokenController.js b/controllers/tokenController.js index 4386e65..63264a9 100644 --- a/controllers/tokenController.js +++ b/controllers/tokenController.js @@ -1,3 +1,4 @@ +const auth = require('./authController') const config = require('./../config') const db = require('knex')(config.database) const randomstring = require('randomstring') @@ -7,17 +8,35 @@ const tokenController = {} tokenController.verify = async (req, res, next) => { const token = req.body.token - if (token === undefined) { return res.status(401).json({ success: false, description: 'No token provided.' }) } + if (token === undefined) { + return res.status(401).json({ + success: false, + description: 'No token provided.' + }) + } const user = await db.table('users').where('token', token).first() - if (!user) { return res.status(401).json({ success: false, description: 'Invalid token.' }) } - return res.json({ success: true, username: user.username }) + if (!user) { + return res.status(401).json({ + success: false, + description: 'Invalid token.' + }) + } + + return res.json({ + success: true, + username: user.username, + permissions: auth.mapPermissions(user) + }) } tokenController.list = async (req, res, next) => { const user = await utils.authorize(req, res) if (!user) { return } - return res.json({ success: true, token: user.token }) + return res.json({ + success: true, + token: user.token + }) } tokenController.change = async (req, res, next) => { @@ -30,7 +49,10 @@ tokenController.change = async (req, res, next) => { timestamp: Math.floor(Date.now() / 1000) }) - res.json({ success: true, token: newtoken }) + res.json({ + success: true, + token: newtoken + }) } module.exports = tokenController diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 980d492..fd25e00 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -1,3 +1,4 @@ +const auth = require('./authController') const config = require('./../config') const crypto = require('crypto') const db = require('knex')(config.database) @@ -649,6 +650,11 @@ uploadsController.list = async (req, res) => { let offset = req.params.page if (offset === undefined) { offset = 0 } + // Headers is string-only, this seem to be the safest and lightest + const all = req.headers.all === '1' + const ismoderator = auth.is(user, 'moderator') + if (all && !ismoderator) { return res.json(403) } + const files = await db.table('files') .where(function () { if (req.params.id === undefined) { @@ -658,7 +664,9 @@ uploadsController.list = async (req, res) => { } }) .where(function () { - if (user.username !== 'root') { this.where('userid', user.id) } + if (!all || !ismoderator) { + this.where('userid', user.id) + } }) .orderBy('id', 'DESC') .limit(25) @@ -668,7 +676,7 @@ uploadsController.list = async (req, res) => { const albums = await db.table('albums') .where(function () { this.where('enabled', 1) - if (user.username !== 'root') { + if (!all || !ismoderator) { this.where('userid', user.id) } }) @@ -688,8 +696,8 @@ uploadsController.list = async (req, res) => { } } - // Only push usernames if we are root - if (user.username === 'root') { + // Only push usernames if we are a moderator + if (all && ismoderator) { if (file.userid !== undefined && file.userid !== null && file.userid !== '') { userids.push(file.userid) } @@ -702,13 +710,12 @@ uploadsController.list = async (req, res) => { } // If we are a normal user, send response - if (user.username !== 'root') { return res.json({ success: true, files }) } + if (!ismoderator) { return res.json({ success: true, files }) } - // If we are root but there are no uploads attached to a user, send response + // If we are a moderator but there are no uploads attached to a user, send response if (userids.length === 0) { return res.json({ success: true, files }) } - const users = await db.table('users') - .whereIn('id', userids) + const users = await db.table('users').whereIn('id', userids) for (const dbUser of users) { for (const file of files) { if (file.userid === dbUser.id) { diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 8793c30..0a47691 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -57,9 +57,18 @@ utilsController.authorize = async (req, res) => { } const user = await db.table('users').where('token', token).first() - if (user) { return user } + if (user) { + if (user.enabled === false || user.enabled === 0) { + res.json({ success: false, description: 'This account has been disabled.' }) + return + } + return user + } - res.status(401).json({ success: false, description: 'Invalid token.' }) + res.status(401).json({ + success: false, + description: 'Invalid token.' + }) } utilsController.generateThumbs = (name, force) => { diff --git a/database/db.js b/database/db.js index b9449ed..d8bbb73 100644 --- a/database/db.js +++ b/database/db.js @@ -1,3 +1,5 @@ +const { permissions } = require('./../controllers/authController') + const init = function (db) { // Create the tables we need to store galleries and files db.schema.hasTable('albums').then(exists => { @@ -44,6 +46,7 @@ const init = function (db) { table.integer('enabled') table.integer('timestamp') table.integer('fileLength') + table.integer('permission') }).then(() => { db.table('users').where({ username: 'root' }).then((user) => { if (user.length > 0) { return } @@ -55,7 +58,8 @@ const init = function (db) { username: 'root', password: hash, token: require('randomstring').generate(64), - timestamp: Math.floor(Date.now() / 1000) + timestamp: Math.floor(Date.now() / 1000), + permission: permissions.superadmin }).then(() => {}) }) }) diff --git a/database/migration.js b/database/migration.js index 64d1dce..86d9b5b 100644 --- a/database/migration.js +++ b/database/migration.js @@ -1,5 +1,6 @@ const config = require('./../config') const db = require('knex')(config.database) +const { permissions } = require('./../controllers/authController') const map = { albums: { @@ -10,7 +11,8 @@ const map = { }, users: { enabled: 'integer', - fileLength: 'integer' + fileLength: 'integer', + permission: 'integer' } } @@ -30,6 +32,14 @@ migration.start = async () => { })) })) + await db.table('users') + .where('username', 'root') + .first() + .update({ + permission: permissions.superadmin + }) + .then(() => console.log(`Updated root's permission to ${permissions.superadmin} (superadmin).`)) + console.log('Migration finished! Now start lolisafe normally') process.exit(0) } diff --git a/lolisafe.js b/lolisafe.js index d76fc9f..3aec125 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -32,9 +32,12 @@ fs.existsSync(`./${config.uploads.folder}/zips`) || fs.mkdirSync(`./${config.upl safe.use(helmet()) safe.set('trust proxy', 1) +// https://mozilla.github.io/nunjucks/api.html#configure nunjucks.configure('views', { autoescape: true, - express: safe + express: safe, + // watch: true, // will this be fine in production? + noCache: process.env.DEV === '1' }) safe.set('view engine', 'njk') safe.enable('view cache') @@ -127,7 +130,13 @@ const start = async () => { if (!created) { return process.exit(1) } } - safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`)) + safe.listen(config.port, () => { + console.log(`lolisafe started on port ${config.port}`) + if (process.env.DEV === '1') { + // DEV=1 yarn start + console.log('lolisafe is in development mode, nunjucks caching disabled') + } + }) } start() diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 19d5e3d..c05cb32 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -9,6 +9,11 @@ .menu-list a { color: #3794d2; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .menu-list a:hover { @@ -21,6 +26,15 @@ background-color: #3794d2; } +.menu-list a[disabled] { + color: #7a7a7a; + cursor: not-allowed; +} + +.menu-list a[disabled]:hover { + background: none; +} + .pagination a { color: #eff0f1; border-color: #4d4d4d; @@ -71,7 +85,7 @@ width: 100%; } -.image-container .file-checkbox { +.image-container .checkbox { position: absolute; top: .75rem; left: .75rem; @@ -118,7 +132,7 @@ /* Make extra info appear on hover only on non-touch devices */ -.no-touch .image-container .file-checkbox { +.no-touch .image-container .checkbox { opacity: .25; -webkit-transition: opacity .25s; transition: opacity .25s; @@ -131,7 +145,7 @@ transition: opacity .25s; } -.no-touch .image-container:hover .file-checkbox, +.no-touch .image-container:hover .checkbox, .no-touch .image-container:hover .controls, .no-touch .image-container:hover .details { opacity: 1; @@ -180,6 +194,6 @@ height: 2.25em; } -.table td a:not([href]) { - text-decoration: line-through; +.is-linethrough { + text-decoration: line-through } diff --git a/public/css/sweetalert.css b/public/css/sweetalert.css index beea10d..aa7cb35 100644 --- a/public/css/sweetalert.css +++ b/public/css/sweetalert.css @@ -18,6 +18,13 @@ color: #bdc3c7; } +.swal-content .is-code { + font-family: 'Courier New', Courier, monospace; + border: 1px dashed #eff0f1; + border-radius: 5px; + margin-top: 5px; +} + .swal-button { background-color: #3794d2; color: #eff0f1; diff --git a/public/js/.eslintrc.json b/public/js/.eslintrc.json index c427cf4..eebc10b 100644 --- a/public/js/.eslintrc.json +++ b/public/js/.eslintrc.json @@ -1,7 +1,7 @@ { "root": true, "parserOptions": { - "ecmaVersion": 5 + "ecmaVersion": 6 }, "env": { "browser": true @@ -17,6 +17,17 @@ "quotes": [ "error", "single" + ], + "object-shorthand": [ + "error", + "always" + ], + "prefer-const": [ + "error", + { + "destructuring": "any", + "ignoreReadBeforeAssign": false + } ] } } diff --git a/public/js/album.js b/public/js/album.js index 7db04b0..6d5ede4 100644 --- a/public/js/album.js +++ b/public/js/album.js @@ -1,6 +1,6 @@ /* global LazyLoad */ -var page = {} +const page = {} window.onload = function () { page.lazyLoad = new LazyLoad() diff --git a/public/js/auth.js b/public/js/auth.js index 331d9c6..a8782dc 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -1,6 +1,6 @@ /* global swal, axios */ -var page = { +const page = { // user token token: localStorage.token, @@ -10,33 +10,31 @@ var page = { } page.do = function (dest) { - var user = page.user.value - var pass = page.pass.value + const user = page.user.value + const pass = page.pass.value if (!user) { - return swal('Error', 'You need to specify a username', 'error') + return swal('An error occurred!', 'You need to specify a username', 'error') } if (!pass) { - return swal('Error', 'You need to specify a username', 'error') + return swal('An error occurred!', 'You need to specify a username', 'error') } - axios.post('api/' + dest, { + axios.post(`api/${dest}`, { username: user, password: pass - }) - .then(function (response) { - if (response.data.success === false) { - return swal('Error', response.data.description, 'error') - } + }).then(function (response) { + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } - localStorage.token = response.data.token - window.location = 'dashboard' - }) - .catch(function (error) { - console.error(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + localStorage.token = response.data.token + window.location = 'dashboard' + }).catch(function (error) { + console.error(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.verify = function () { @@ -44,18 +42,16 @@ page.verify = function () { axios.post('api/tokens/verify', { token: page.token - }) - .then(function (response) { - if (response.data.success === false) { - return swal('Error', response.data.description, 'error') - } + }).then(function (response) { + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } - window.location = 'dashboard' - }) - .catch(function (error) { - console.error(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + window.location = 'dashboard' + }).catch(function (error) { + console.error(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } window.onload = function () { diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 0e64ea0..baddaca 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -1,30 +1,66 @@ /* global swal, axios, ClipboardJS, LazyLoad */ -var page = { +// keys for localStorage +const LS_KEYS = { + token: 'token', + viewType: { + uploads: 'UPLOADS_VIEW_TYPE' + }, + selected: { + uploads: 'SELECTED_UPLOADS', + users: 'SELECTED_USERS' + } +} + +const page = { // #page dom: null, // user token - token: localStorage.token, - username: null, // from api/tokens/verify + token: localStorage[LS_KEYS.token], - // view config (either list or thumbs) - filesView: localStorage.filesView, + // from api/tokens/verify + username: null, + permissions: null, - // current view (which album and which page) - currentView: { album: null, pageNum: null }, + currentView: null, + views: { + // config of uploads view + uploads: { + type: localStorage[LS_KEYS.viewType.uploads], + album: null, // album's id + pageNum: null, // page num + all: null // listing all uploads or just the user's + }, + // config of users view + users: { + pageNum: null + } + }, - // id of selected files (shared across pages and will be synced with localStorage) - selectedFiles: [], - checkboxes: [], - lastSelected: null, + // id of selected items (shared across pages and will be synced with localStorage) + selected: { + uploads: [], + users: [] + }, + checkboxes: { + uploads: [], + users: [] + }, + lastSelected: { + upload: null, + user: null + }, // select album dom for dialogs/modals selectAlbumContainer: null, - // cache of files and albums data for dialogs/modals - files: {}, - albums: {}, + // cache for dialogs/modals + cache: { + uploads: {}, + albums: {}, + users: {} + }, clipboardJS: null, lazyLoad: null, @@ -42,31 +78,30 @@ page.preparePage = function () { } page.verifyToken = function (token, reloadOnError) { - axios.post('api/tokens/verify', { token: token }) - .then(function (response) { - if (response.data.success === false) { - return swal({ - title: 'An error occurred!', - text: response.data.description, - icon: 'error' - }) - .then(function () { - if (!reloadOnError) { return } - localStorage.removeItem('token') - location.location = 'auth' - }) - } + axios.post('api/tokens/verify', { token }).then(function (response) { + if (response.data.success === false) { + return swal({ + title: 'An error occurred!', + text: response.data.description, + icon: 'error' + }).then(function () { + if (!reloadOnError) { return } + localStorage.removeItem(LS_KEYS.token) + location.location = 'auth' + }) + } - axios.defaults.headers.common.token = token - localStorage.token = token - page.token = token - page.username = response.data.username - page.prepareDashboard() - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + axios.defaults.headers.common.token = token + localStorage[LS_KEYS.token] = token + page.token = token + page.username = response.data.username + page.permissions = response.data.permissions + console.log(page.username, page.permissions) + page.prepareDashboard() + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.prepareDashboard = function () { @@ -76,9 +111,27 @@ page.prepareDashboard = function () { document.getElementById('auth').style.display = 'none' document.getElementById('dashboard').style.display = 'block' + if (page.permissions.moderator) { + const itemManageUploads = document.getElementById('itemManageUploads') + itemManageUploads.removeAttribute('disabled') + itemManageUploads.addEventListener('click', function () { + page.setActiveMenu(this) + page.getUploads({ all: true }) + }) + } + + if (page.permissions.admin) { + const itemManageUsers = document.getElementById('itemManageUsers') + itemManageUsers.removeAttribute('disabled') + itemManageUsers.addEventListener('click', function () { + page.setActiveMenu(this) + page.getUsers() + }) + } + document.getElementById('itemUploads').addEventListener('click', function () { page.setActiveMenu(this) - page.getUploads() + page.getUploads({ all: false }) }) document.getElementById('itemDeleteByNames').addEventListener('click', function () { @@ -106,11 +159,11 @@ page.prepareDashboard = function () { page.changePassword() }) - var logoutBtn = document.getElementById('itemLogout') + const logoutBtn = document.getElementById('itemLogout') logoutBtn.addEventListener('click', function () { page.logout() }) - logoutBtn.innerHTML = 'Logout ( ' + page.username + ' )' + logoutBtn.innerHTML = `Logout ( ${page.username} )` page.getAlbumsSidebar() @@ -118,20 +171,20 @@ page.prepareDashboard = function () { } page.logout = function () { - localStorage.removeItem('token') + localStorage.removeItem(LS_KEYS.token) location.reload('.') } page.getItemID = function (element) { // This expects the item's parent to have the item's ID - var parent = element.parentNode + let parent = element.parentNode // If the element is part of a set of controls, use the container's parent instead if (element.parentNode.classList.contains('controls')) { parent = parent.parentNode } return parseInt(parent.dataset.id) } page.domClick = function (event) { - var element = event.target + let element = event.target if (!element) { return } // If the clicked element is an icon, delegate event to its A parent; hacky @@ -142,35 +195,54 @@ page.domClick = function (event) { if (!element.dataset || !element.dataset.action) { return } event.stopPropagation() // maybe necessary - var id = page.getItemID(element) - var action = element.dataset.action + const id = page.getItemID(element) + const action = element.dataset.action + + // Handle pagination actions + if (['page-prev', 'page-next'].includes(action)) { + const views = {} + let func = null + + if (page.currentView === 'uploads') { + views.album = page.views.uploads.album + views.all = page.views.uploads.all + func = page.getUploads + } else if (page.currentView === 'users') { + func = page.getUsers + } + + switch (action) { + case 'page-prev': + views.pageNum = page.views[page.currentView].pageNum - 1 + if (views.pageNum < 0) { + return swal('An error occurred!', 'This is already the first page.', 'error') + } + return func(views, element) + case 'page-next': + views.pageNum = page.views[page.currentView].pageNum + 1 + return func(views, element) + } + } switch (action) { - case 'page-prev': - if (page.currentView.pageNum === 0) { - return swal('Can\'t do that!', 'This is already the first page!', 'warning') - } - return page.getUploads(page.currentView.album, page.currentView.pageNum - 1, element) - case 'page-next': - return page.getUploads(page.currentView.album, page.currentView.pageNum + 1, element) case 'view-list': - return page.setFilesView('list', element) + return page.setUploadsView('list', element) case 'view-thumbs': - return page.setFilesView('thumbs', element) + return page.setUploadsView('thumbs', element) case 'clear-selection': return page.clearSelection() case 'add-selected-files-to-album': return page.addSelectedFilesToAlbum() case 'bulk-delete': return page.deleteSelectedFiles() - case 'select-file': - return page.selectFile(element, event) + case 'select': + return page.select(element, event) case 'add-to-album': return page.addSingleFileToAlbum(id) case 'delete-file': return page.deleteFile(id) - case 'select-all-files': - return page.selectAllFiles(element) + case 'select-all': + return page.selectAll(element) case 'display-thumbnail': return page.displayThumbnail(id) case 'delete-file-by-names': @@ -183,6 +255,8 @@ page.domClick = function (event) { return page.deleteAlbum(id) case 'get-new-token': return page.getNewToken(element) + case 'edit-user': + return page.editUser(id) } } @@ -192,245 +266,257 @@ page.isLoading = function (element, state) { element.classList.remove('is-loading') } -page.getUploads = function (album, pageNum, element) { +page.getUploads = function ({ album, pageNum, all } = {}, element) { if (element) { page.isLoading(element, true) } if (pageNum === undefined) { pageNum = 0 } - var url = 'api/uploads/' + pageNum - if (album !== undefined) { url = 'api/album/' + album + '/' + pageNum } + let url = `api/uploads/${pageNum}` + if (album !== undefined) { url = `api/album/${album}/${pageNum}` } - axios.get(url) - .then(function (response) { - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } - } + if (all && !page.permissions.moderator) { + return swal('An error occurred!', 'You can not do this!', 'error') + } - if (pageNum && (response.data.files.length === 0)) { - // Only remove loading class here, since beyond this the entire page will be replaced anyways - if (element) { page.isLoading(element, false) } - return swal('Can\'t do that!', 'There are no more files!', 'warning') - } - - page.files = {} - - var pagination = - '' - - var controls = - '
\n' + - '
\n' + - ' \n' + - ' \n' + - '
' - - var allFilesSelected = true - var table, i, file, selected, displayAlbumOrUser - - if (page.filesView === 'thumbs') { - page.dom.innerHTML = - pagination + '\n' + - '
\n' + - controls + '\n' + - '
\n' + - '
\n' + - pagination - - table = document.getElementById('table') - - for (i = 0; i < response.data.files.length; i++) { - file = response.data.files[i] - selected = page.selectedFiles.includes(file.id) - if (!selected && allFilesSelected) { allFilesSelected = false } - - // Prettify - file.prettyBytes = page.getPrettyBytes(parseInt(file.size)) - file.prettyDate = page.getPrettyDate(new Date(file.timestamp * 1000)) - - displayAlbumOrUser = file.album - if (page.username === 'root') { - displayAlbumOrUser = '' - if (file.username !== undefined) { displayAlbumOrUser = file.username } - } - - var div = document.createElement('div') - div.className = 'image-container column is-narrow' - div.dataset.id = file.id - if (file.thumb !== undefined) { - div.innerHTML = '' + file.name + '' - } else { - div.innerHTML = '

' + (file.extname || 'N/A') + '

' - } - - div.innerHTML += - '\n' + - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
\n' + - '
\n' + - '

' + file.name + '

\n' + - '

' + (displayAlbumOrUser ? ('' + displayAlbumOrUser + ' – ') : '') + file.prettyBytes + '

\n' + - '
' - - table.appendChild(div) - page.checkboxes = Array.from(table.getElementsByClassName('file-checkbox')) - page.lazyLoad.update() - } + console.log(url, all) + axios.get(url, { + headers: { + all: all ? '1' : '0' + } + }).then(function (response) { + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) } else { - var albumOrUser = 'Album' - if (page.username === 'root') { albumOrUser = 'User' } + return swal('An error occurred!', response.data.description, 'error') + } + } - page.dom.innerHTML = - pagination + '\n' + - '
\n' + - controls + '\n' + - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
File' + albumOrUser + 'SizeDate
\n' + - '
\n' + - '
\n' + - pagination + if (pageNum && (response.data.files.length === 0)) { + // Only remove loading class here, since beyond this the entire page will be replaced anyways + if (element) { page.isLoading(element, false) } + return swal('An error occurred!', 'There are no more uploads.', 'error') + } - table = document.getElementById('table') + page.currentView = 'uploads' + page.cache.uploads = {} - for (i = 0; i < response.data.files.length; i++) { - file = response.data.files[i] - selected = page.selectedFiles.includes(file.id) - if (!selected && allFilesSelected) { allFilesSelected = false } + const pagination = ` + + ` - page.files[file.id] = { - name: file.name, - thumb: file.thumb - } + const controls = ` +
+
+ + +
+ ` - // Prettify - file.prettyBytes = page.getPrettyBytes(parseInt(file.size)) - file.prettyDate = page.getPrettyDate(new Date(file.timestamp * 1000)) + let allSelected = true + if (page.views.uploads.type === 'thumbs') { + page.dom.innerHTML = ` + ${pagination} +
+ ${controls} +
+
+ ${pagination} + ` - displayAlbumOrUser = file.album - if (page.username === 'root') { - displayAlbumOrUser = '' - if (file.username !== undefined) { displayAlbumOrUser = file.username } - } + const table = document.getElementById('table') - var tr = document.createElement('tr') - tr.dataset.id = file.id - tr.innerHTML = - '\n' + - '' + file.name + '\n' + - '' + displayAlbumOrUser + '\n' + - '' + file.prettyBytes + '\n' + - '' + file.prettyDate + '\n' + - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '' + for (let i = 0; i < response.data.files.length; i++) { + const upload = response.data.files[i] + const selected = page.selected.uploads.includes(upload.id) + if (!selected && allSelected) { allSelected = false } - table.appendChild(tr) - page.checkboxes = Array.from(table.getElementsByClassName('file-checkbox')) + // Prettify + upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size)) + upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000)) + + let displayAlbumOrUser = upload.album + if (all) { displayAlbumOrUser = upload.username || '' } + + const div = document.createElement('div') + div.className = 'image-container column is-narrow' + div.dataset.id = upload.id + if (upload.thumb !== undefined) { + div.innerHTML = `${upload.name}` + } else { + div.innerHTML = `

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

` } - } - if (allFilesSelected && response.data.files.length) { - var selectAll = document.getElementById('selectAll') - if (selectAll) { selectAll.checked = true } - } + div.innerHTML += ` + +
+ + + + + + + + + + + + + + + +
+
+

${upload.name}

+

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

+
+ ` - page.currentView.album = album - page.currentView.pageNum = response.data.files.length ? pageNum : 0 - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + table.appendChild(div) + // page.checkboxes.uploads = Array.from(table.getElementsByClassName('checkbox')) + page.checkboxes.uploads = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) + page.lazyLoad.update() + } + } else { + let albumOrUser = 'Album' + if (all) { albumOrUser = 'User' } + + page.dom.innerHTML = ` + ${pagination} +
+ ${controls} +
+ + + + + + + + + + + + + +
File${albumOrUser}SizeDate
+
+
+ ${pagination} + ` + + const table = document.getElementById('table') + + for (let i = 0; i < response.data.files.length; i++) { + const upload = response.data.files[i] + const selected = page.selected.uploads.includes(upload.id) + if (!selected && allSelected) { allSelected = false } + + page.cache.uploads[upload.id] = { + name: upload.name, + thumb: upload.thumb + } + + // Prettify + upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size)) + upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000)) + + let displayAlbumOrUser = upload.album + if (all) { displayAlbumOrUser = upload.username || '' } + + const tr = document.createElement('tr') + tr.dataset.id = upload.id + tr.innerHTML = ` + + ${upload.name} + ${displayAlbumOrUser} + ${upload.prettyBytes} + ${upload.prettyDate} + + + + + + + + + + + + ${all ? '' : ` + + + + + `} + + + + + + + ` + + table.appendChild(tr) + // page.checkboxes.uploads = Array.from(table.getElementsByClassName('checkbox')) + page.checkboxes.uploads = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) + } + } + + if (allSelected && response.data.files.length) { + const selectAll = document.getElementById('selectAll') + if (selectAll) { selectAll.checked = true } + } + + page.views.uploads.album = album + page.views.uploads.pageNum = response.data.files.length ? pageNum : 0 + page.views.uploads.all = all + }).catch(function (error) { + if (element) { page.isLoading(element, false) } + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } -page.setFilesView = function (view, element) { - localStorage.filesView = view - page.filesView = view - page.getUploads(page.currentView.album, page.currentView.pageNum, element) +page.setUploadsView = function (view, element) { + localStorage[LS_KEYS.viewType.files] = view + page.views.uploads.type = view + page.getUploads(page.views.uploads, element) } page.displayThumbnail = function (id) { - var file = page.files[id] + const file = page.cache.uploads[id] if (!file.thumb) { return } return swal({ text: file.name, @@ -442,113 +528,123 @@ page.displayThumbnail = function (id) { }) } -page.selectAllFiles = function (element) { - var table = document.getElementById('table') - var checkboxes = table.getElementsByClassName('file-checkbox') +page.selectAll = function (element) { + const currentView = page.currentView + const checkboxes = page.checkboxes[currentView] + const selected = page.selected[currentView] - for (var i = 0; i < checkboxes.length; i++) { - var id = page.getItemID(checkboxes[i]) + for (let i = 0; i < checkboxes.length; i++) { + const id = page.getItemID(checkboxes[i]) if (isNaN(id)) { continue } if (checkboxes[i].checked !== element.checked) { checkboxes[i].checked = element.checked if (checkboxes[i].checked) { - page.selectedFiles.push(id) + selected.push(id) } else { - page.selectedFiles.splice(page.selectedFiles.indexOf(id), 1) + selected.splice(selected.indexOf(id), 1) } } } - if (page.selectedFiles.length) { - localStorage.selectedFiles = JSON.stringify(page.selectedFiles) + if (selected.length) { + localStorage[LS_KEYS.selected[currentView]] = JSON.stringify(selected) } else { - localStorage.removeItem('selectedFiles') + localStorage.removeItem(LS_KEYS.selected[currentView]) } - element.title = element.checked ? 'Unselect all files' : 'Select all files' + page.selected[currentView] = selected + element.title = element.checked ? 'Unselect all uploads' : 'Select all uploads' } page.selectInBetween = function (element, lastElement) { if (!element || !lastElement) { return } if (element === lastElement) { return } - if (!page.checkboxes || !page.checkboxes.length) { return } - var thisIndex = page.checkboxes.indexOf(element) - var lastIndex = page.checkboxes.indexOf(lastElement) + const currentView = page.currentView + const checkboxes = page.checkboxes[currentView] + if (!checkboxes || !checkboxes.length) { return } - var distance = thisIndex - lastIndex + const thisIndex = checkboxes.indexOf(element) + const lastIndex = checkboxes.indexOf(lastElement) + + const distance = thisIndex - lastIndex if (distance >= -1 && distance <= 1) { return } - for (var i = 0; i < page.checkboxes.length; i++) { + for (let i = 0; i < checkboxes.length; i++) { if ((thisIndex > lastIndex && i > lastIndex && i < thisIndex) || (thisIndex < lastIndex && i > thisIndex && i < lastIndex)) { - page.checkboxes[i].checked = true - page.selectedFiles.push(page.getItemID(page.checkboxes[i])) + checkboxes[i].checked = true + page.selected[currentView].push(page.getItemID(checkboxes[i])) } } - localStorage.selectedFiles = JSON.stringify(page.selectedFiles) + localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected[currentView]) + page.checkboxes[currentView] = checkboxes } -page.selectFile = function (element, event) { - if (event.shiftKey && page.lastSelected) { - page.selectInBetween(element, page.lastSelected) +page.select = function (element, event) { + const currentView = page.currentView + const lastSelected = page.lastSelected[currentView] + if (event.shiftKey && lastSelected) { + page.selectInBetween(element, lastSelected) } else { - page.lastSelected = element + page.lastSelected[currentView] = element } - var id = page.getItemID(element) + const id = page.getItemID(element) if (isNaN(id)) { return } - if (!page.selectedFiles.includes(id) && element.checked) { - page.selectedFiles.push(id) - } else if (page.selectedFiles.includes(id) && !element.checked) { - page.selectedFiles.splice(page.selectedFiles.indexOf(id), 1) + const selected = page.selected[currentView] + if (!selected.includes(id) && element.checked) { + selected.push(id) + } else if (selected.includes(id) && !element.checked) { + selected.splice(selected.indexOf(id), 1) } - if (page.selectedFiles.length) { - localStorage.selectedFiles = JSON.stringify(page.selectedFiles) + if (selected.length) { + localStorage[LS_KEYS.selected[currentView]] = JSON.stringify(selected) } else { - localStorage.removeItem('selectedFiles') + localStorage.removeItem(LS_KEYS.selected[currentView]) } + + page.selected[currentView] = selected } page.clearSelection = function () { - var count = page.selectedFiles.length + const currentView = page.currentView + const selected = page.selected[currentView] + const count = selected.length if (!count) { - return swal('An error occurred!', 'You have not selected any files.', 'error') + return swal('An error occurred!', `You have not selected any ${currentView}.`, 'error') } - var suffix = 'file' + (count === 1 ? '' : 's') + const suffix = count === 1 ? currentView.substring(0, currentView.length - 1) : currentView return swal({ title: 'Are you sure?', - text: 'You are going to unselect ' + count + ' ' + suffix + '.', + text: `You are going to unselect ${count} ${suffix}.`, buttons: true - }) - .then(function (proceed) { - if (!proceed) { return } + }).then(function (proceed) { + if (!proceed) { return } - var table = document.getElementById('table') - var checkboxes = table.getElementsByClassName('file-checkbox') - - for (var i = 0; i < checkboxes.length; i++) { - if (checkboxes[i].checked) { - checkboxes[i].checked = false - } + const checkboxes = page.checkboxes[currentView] + for (let i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].checked) { + checkboxes[i].checked = false } + } - page.selectedFiles = [] - localStorage.removeItem('selectedFiles') + localStorage.removeItem(LS_KEYS.selected[currentView]) + page.selected[currentView] = [] - var selectAll = document.getElementById('selectAll') - if (selectAll) { selectAll.checked = false } + const selectAll = document.getElementById('selectAll') + if (selectAll) { selectAll.checked = false } - return swal('Cleared selection!', 'Unselected ' + count + ' ' + suffix + '.', 'success') - }) + return swal('Cleared selection!', `Unselected ${count} ${suffix}.`, 'success') + }) } page.deleteFile = function (id) { - // TODO: Share function with bulk delete, just like 'add selected files to album' and 'add single file to album' + // TODO: Share function with bulk delete, just like 'add selected uploads to album' and 'add single file to album' swal({ title: 'Are you sure?', text: 'You won\'t be able to recover the file!', @@ -561,221 +657,214 @@ page.deleteFile = function (id) { closeModal: false } } - }) - .then(function (proceed) { - if (!proceed) { return } + }).then(function (proceed) { + if (!proceed) { return } - axios.post('api/upload/delete', { id: id }) - .then(function (response) { - if (!response) { return } + axios.post('api/upload/delete', { id }).then(function (response) { + if (!response) { return } - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } - swal('Deleted!', 'The file has been deleted.', 'success') - page.getUploads(page.currentView.album, page.currentView.pageNum) - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + swal('Deleted!', 'The file has been deleted.', 'success') + page.getUploads(page.views.uploads) + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } page.deleteSelectedFiles = function () { - var count = page.selectedFiles.length + const count = page.selected.uploads.length if (!count) { - return swal('An error occurred!', 'You have not selected any files.', 'error') + return swal('An error occurred!', 'You have not selected any uploads.', 'error') } - var suffix = 'file' + (count === 1 ? '' : 's') + const suffix = `file${count === 1 ? '' : 's'}` swal({ title: 'Are you sure?', - text: 'You won\'t be able to recover ' + count + ' ' + suffix + '!', + text: `You won't be able to recover ${count} ${suffix}!`, icon: 'warning', dangerMode: true, buttons: { cancel: true, confirm: { - text: 'Yes, nuke the ' + suffix + '!', + text: `Yes, nuke the ${suffix}!`, closeModal: false } } - }) - .then(function (proceed) { - if (!proceed) { return } + }).then(function (proceed) { + if (!proceed) { return } - axios.post('api/upload/bulkdelete', { - field: 'id', - values: page.selectedFiles - }) - .then(function (bulkdelete) { - if (!bulkdelete) { return } + axios.post('api/upload/bulkdelete', { + field: 'id', + values: page.selected.uploads + }).then(function (bulkdelete) { + if (!bulkdelete) { return } - if (bulkdelete.data.success === false) { - if (bulkdelete.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', bulkdelete.data.description, 'error') - } - } + if (bulkdelete.data.success === false) { + if (bulkdelete.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', bulkdelete.data.description, 'error') + } + } - var deleted = count - if (bulkdelete.data.failed && bulkdelete.data.failed.length) { - deleted -= bulkdelete.data.failed.length - page.selectedFiles = page.selectedFiles.filter(function (id) { - return bulkdelete.data.failed.includes(id) - }) - } else { - page.selectedFiles = [] - } - - localStorage.selectedFiles = JSON.stringify(page.selectedFiles) - - swal('Deleted!', deleted + ' file' + (deleted === 1 ? ' has' : 's have') + ' been deleted.', 'success') - return page.getUploads(page.currentView.album, page.currentView.pageNum) - }) - .catch(function (error) { - console.log(error) - swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + let deleted = count + if (bulkdelete.data.failed && bulkdelete.data.failed.length) { + deleted -= bulkdelete.data.failed.length + page.selected.uploads = page.selected.uploads.filter(function (id) { + return bulkdelete.data.failed.includes(id) }) + } else { + page.selected.uploads = [] + } + + localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected.uploads) + + swal('Deleted!', `${deleted} file${deleted === 1 ? ' has' : 's have'} been deleted.`, 'success') + return page.getUploads(page.views.uploads) + }).catch(function (error) { + console.log(error) + swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } page.deleteByNames = function () { - page.dom.innerHTML = - '

Delete by names

\n' + - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '

Separate each entry with a new line.

\n' + - '
\n' + - '
\n' + - ' \n' + - '
' + page.dom.innerHTML = ` +

Delete by names

+
+ +
+ +
+

Separate each entry with a new line.

+
+
+ +
+ ` } page.deleteFileByNames = function () { - var names = document.getElementById('names').value + const names = document.getElementById('names').value .split(/\r?\n/) .filter(function (n) { return n.trim().length }) - var count = names.length + const count = names.length if (!count) { return swal('An error occurred!', 'You have not entered any file names.', 'error') } - var suffix = 'file' + (count === 1 ? '' : 's') + const suffix = `file${count === 1 ? '' : 's'}` swal({ title: 'Are you sure?', - text: 'You won\'t be able to recover ' + count + ' ' + suffix + '!', + text: `You won't be able to recover ${count} ${suffix}!`, icon: 'warning', dangerMode: true, buttons: { cancel: true, confirm: { - text: 'Yes, nuke the ' + suffix + '!', + text: `Yes, nuke the ${suffix}!`, closeModal: false } } - }) - .then(function (proceed) { - if (!proceed) { return } + }).then(function (proceed) { + if (!proceed) { return } - axios.post('api/upload/bulkdelete', { - field: 'name', - values: names - }) - .then(function (bulkdelete) { - if (!bulkdelete) { return } + axios.post('api/upload/bulkdelete', { + field: 'name', + values: names + }).then(function (bulkdelete) { + if (!bulkdelete) { return } - if (bulkdelete.data.success === false) { - if (bulkdelete.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', bulkdelete.data.description, 'error') - } - } + if (bulkdelete.data.success === false) { + if (bulkdelete.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', bulkdelete.data.description, 'error') + } + } - var deleted = count - if (bulkdelete.data.failed && bulkdelete.data.failed.length) { - deleted -= bulkdelete.data.failed.length - } + let deleted = count + if (bulkdelete.data.failed && bulkdelete.data.failed.length) { + deleted -= bulkdelete.data.failed.length + } - document.getElementById('names').value = bulkdelete.data.failed.join('\n') - swal('Deleted!', deleted + ' file' + (deleted === 1 ? ' has' : 's have') + ' been deleted.', 'success') - }) - .catch(function (error) { - console.log(error) - swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + document.getElementById('names').value = bulkdelete.data.failed.join('\n') + swal('Deleted!', `${deleted} file${deleted === 1 ? ' has' : 's have'} been deleted.`, 'success') + }).catch(function (error) { + console.log(error) + swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } page.addSelectedFilesToAlbum = function () { - var count = page.selectedFiles.length + const count = page.selected.uploads.length if (!count) { - return swal('An error occurred!', 'You have not selected any files.', 'error') + return swal('An error occurred!', 'You have not selected any uploads.', 'error') } - page.addFilesToAlbum(page.selectedFiles, function (failed) { + page.addFilesToAlbum(page.selected.uploads, function (failed) { if (!failed) { return } if (failed.length) { - page.selectedFiles = page.selectedFiles.filter(function (id) { + page.selected.uploads = page.selected.uploads.filter(function (id) { return failed.includes(id) }) } else { - page.selectedFiles = [] + page.selected.uploads = [] } - localStorage.selectedFiles = JSON.stringify(page.selectedFiles) - page.getUploads(page.currentView.album, page.currentView.pageNum) + localStorage[LS_KEYS.selected.uploads] = JSON.stringify(page.selected.uploads) + page.getUploads(page.views.uploads) }) } page.addSingleFileToAlbum = function (id) { page.addFilesToAlbum([id], function (failed) { if (!failed) { return } - page.getUploads(page.currentView.album, page.currentView.pageNum) + page.getUploads(page.views.uploads) }) } page.addFilesToAlbum = function (ids, callback) { - var count = ids.length + const count = ids.length - var content = document.createElement('div') + const content = document.createElement('div') content.id = 'addFilesToAlbum' - content.innerHTML = - '

You are about to add ' + count + ' file' + (count === 1 ? '' : 's') + ' to an album.

' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
' + content.innerHTML = ` +

You are about to add ${count} file${count === 1 ? '' : 's'} to an album.

+
+ +
+
+ +
+
+ ` swal({ title: 'Add to album', icon: 'warning', - content: content, + content, buttons: { cancel: true, confirm: { @@ -786,16 +875,16 @@ page.addFilesToAlbum = function (ids, callback) { }).then(function (choose) { if (!choose) { return } - var container = document.getElementById('addFilesToAlbum') - var select = container.getElementsByTagName('select')[0] - var albumid = parseInt(select.value) + const container = document.getElementById('addFilesToAlbum') + const select = container.getElementsByTagName('select')[0] + const albumid = parseInt(select.value) if (isNaN(albumid)) { return swal('An error occurred!', 'You did not choose an album.', 'error') } axios.post('api/albums/addfiles', { - ids: ids, - albumid: albumid + ids, + albumid }).then(function (add) { if (!add) { return } @@ -808,17 +897,17 @@ page.addFilesToAlbum = function (ids, callback) { return } - var added = ids.length + let added = ids.length if (add.data.failed && add.data.failed.length) { added -= add.data.failed.length } - var suffix = 'file' + (ids.length === 1 ? '' : 's') + const suffix = `file${ids.length === 1 ? '' : 's'}` if (!added) { - return swal('An error occurred!', 'Could not add the ' + suffix + ' to the album.', 'error') + return swal('An error occurred!', `Could not add the ${suffix} to the album.`, 'error') } - swal('Woohoo!', 'Successfully ' + (albumid < 0 ? 'removed' : 'added') + ' ' + added + ' ' + suffix + ' ' + (albumid < 0 ? 'from' : 'to') + ' the album.', 'success') + swal('Woohoo!', `Successfully ${albumid < 0 ? 'removed' : 'added'} ${added} ${suffix} ${albumid < 0 ? 'from' : 'to'} the album.`, 'success') return callback(add.data.failed) }).catch(function (error) { console.log(error) @@ -841,12 +930,12 @@ page.addFilesToAlbum = function (ids, callback) { } // If the prompt was replaced, the container would be missing - var container = document.getElementById('addFilesToAlbum') + const container = document.getElementById('addFilesToAlbum') if (!container) { return } - var select = container.getElementsByTagName('select')[0] + const select = container.getElementsByTagName('select')[0] select.innerHTML += list.data.albums .map(function (album) { - return '' + return `` }) .join('\n') select.getElementsByTagName('option')[1].innerHTML = 'Choose an album' @@ -858,149 +947,150 @@ page.addFilesToAlbum = function (ids, callback) { } page.getAlbums = function () { - axios.get('api/albums') - .then(function (response) { - if (!response) { return } + axios.get('api/albums').then(function (response) { + if (!response) { return } - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } + + page.cache.albums = {} + + page.dom.innerHTML = ` +

Create new album

+
+
+ +
+
+ +
+

List of albums

+
+ + + + + + + + + + + + + +
IDNameFilesCreated atPublic link
+
+ ` + + const homeDomain = response.data.homeDomain + const table = document.getElementById('table') + + for (let i = 0; i < response.data.albums.length; i++) { + const album = response.data.albums[i] + const albumUrl = `${homeDomain}/a/${album.identifier}` + + // Prettify + album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000)) + + page.cache.albums[album.id] = { + name: album.name, + download: album.download, + public: album.public } - page.albums = {} + const tr = document.createElement('tr') + tr.innerHTML = ` + + ${album.id} + ${album.name} + ${album.files} + ${album.prettyDate} + ${albumUrl} + + + + + + + + + + + + + + + + + + + + + + + + ` - page.dom.innerHTML = - '

Create new album

\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '

List of albums

\n' + - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
IDNameFilesCreated atPublic link
\n' + - '
' - - var homeDomain = response.data.homeDomain - var table = document.getElementById('table') - - for (var i = 0; i < response.data.albums.length; i++) { - var album = response.data.albums[i] - var albumUrl = homeDomain + '/a/' + album.identifier - - // Prettify - album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000)) - - page.albums[album.id] = { - name: album.name, - download: album.download, - public: album.public - } - - var tr = document.createElement('tr') - tr.innerHTML = - '\n' + - ' ' + album.id + '\n' + - ' ' + album.name + '\n' + - ' ' + album.files + '\n' + - ' ' + album.prettyDate + '\n' + - ' ' + albumUrl + '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '' - - table.appendChild(tr) - } - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + table.appendChild(tr) + } + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.editAlbum = function (id) { - var album = page.albums[id] + const album = page.cache.albums[id] if (!album) { return } - var div = document.createElement('div') - div.innerHTML = - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
' + const div = document.createElement('div') + div.innerHTML = ` +
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ ` swal({ title: 'Edit album', @@ -1012,50 +1102,47 @@ page.editAlbum = function (id) { closeModal: false } } - }) - .then(function (value) { - if (!value) { return } + }).then(function (value) { + if (!value) { return } - axios.post('api/albums/edit', { - id: id, - name: document.getElementById('_name').value, - download: document.getElementById('_download').checked, - public: document.getElementById('_public').checked, - requestLink: document.getElementById('_requestLink').checked - }) - .then(function (response) { - if (!response) { return } + axios.post('api/albums/edit', { + id, + name: document.getElementById('swalName').value, + download: document.getElementById('swalDownload').checked, + public: document.getElementById('swalPublic').checked, + requestLink: document.getElementById('swalRequestLink').checked + }).then(function (response) { + if (!response) { return } - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } - if (response.data.identifier) { - swal('Success!', 'Your album\'s new identifier is: ' + response.data.identifier + '.', 'success') - } else if (response.data.name !== album.name) { - swal('Success!', 'Your album was renamed to: ' + response.data.name + '.', 'success') - } else { - swal('Success!', 'Your album was edited!', 'success') - } + if (response.data.identifier) { + swal('Success!', `Your album's new identifier is: ${response.data.identifier}.`, 'success') + } else if (response.data.name !== album.name) { + swal('Success!', `Your album was renamed to: ${response.data.name}.`, 'success') + } else { + swal('Success!', 'Your album was edited!', 'success') + } - page.getAlbumsSidebar() - page.getAlbums() - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + page.getAlbumsSidebar() + page.getAlbums() + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } page.deleteAlbum = function (id) { swal({ title: 'Are you sure?', - text: 'This won\'t delete your files, only the album!', + text: 'This won\'t delete your uploads, only the album!', icon: 'warning', dangerMode: true, buttons: { @@ -1065,38 +1152,35 @@ page.deleteAlbum = function (id) { closeModal: false }, purge: { - text: 'Umm, delete the files too please?', + text: 'Umm, delete the uploads too please?', value: 'purge', className: 'swal-button--danger', closeModal: false } } - }) - .then(function (proceed) { - if (!proceed) { return } + }).then(function (proceed) { + if (!proceed) { return } - axios.post('api/albums/delete', { - id: id, - purge: proceed === 'purge' - }) - .then(function (response) { - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } - } + axios.post('api/albums/delete', { + id, + purge: proceed === 'purge' + }).then(function (response) { + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } - swal('Deleted!', 'Your album has been deleted.', 'success') - page.getAlbumsSidebar() - page.getAlbums() - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + swal('Deleted!', 'Your album has been deleted.', 'success') + page.getAlbumsSidebar() + page.getAlbums() + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } page.submitAlbum = function (element) { @@ -1104,68 +1188,64 @@ page.submitAlbum = function (element) { axios.post('api/albums', { name: document.getElementById('albumName').value - }) - .then(function (response) { - if (!response) { return } + }).then(function (response) { + if (!response) { return } - page.isLoading(element, false) + page.isLoading(element, false) - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - swal('Woohoo!', 'Album was created successfully', 'success') - page.getAlbumsSidebar() - page.getAlbums() - }) - .catch(function (error) { - console.log(error) - page.isLoading(element, false) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + swal('Woohoo!', 'Album was created successfully', 'success') + page.getAlbumsSidebar() + page.getAlbums() + }).catch(function (error) { + console.log(error) + page.isLoading(element, false) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.getAlbumsSidebar = function () { - axios.get('api/albums/sidebar') - .then(function (response) { - if (!response) { return } + axios.get('api/albums/sidebar').then(function (response) { + if (!response) { return } - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - var albumsContainer = document.getElementById('albumsContainer') - albumsContainer.innerHTML = '' + const albumsContainer = document.getElementById('albumsContainer') + albumsContainer.innerHTML = '' - if (response.data.albums === undefined) { return } + if (response.data.albums === undefined) { return } - for (var i = 0; i < response.data.albums.length; i++) { - var album = response.data.albums[i] - var li = document.createElement('li') - var a = document.createElement('a') - a.id = album.id - a.innerHTML = album.name + for (let i = 0; i < response.data.albums.length; i++) { + const album = response.data.albums[i] + const li = document.createElement('li') + const a = document.createElement('a') + a.id = album.id + a.innerHTML = album.name - a.addEventListener('click', function () { - page.getAlbum(this) - }) + a.addEventListener('click', function () { + page.getAlbum(this) + }) - li.appendChild(a) - albumsContainer.appendChild(li) - } - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + li.appendChild(a) + albumsContainer.appendChild(li) + } + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.getAlbum = function (album) { @@ -1174,176 +1254,174 @@ page.getAlbum = function (album) { } page.changeFileLength = function () { - axios.get('api/filelength/config') - .then(function (response) { - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + axios.get('api/filelength/config').then(function (response) { + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - page.dom.innerHTML = - '

File name length

\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '

Default file name length is ' + response.data.config.default + ' characters. ' + (response.data.config.userChangeable ? ('Range allowed for user is ' + response.data.config.min + ' to ' + response.data.config.max + ' characters.') : 'Changing file name length is disabled at the moment.') + '

\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
' + // Shorter vars + const { max, min } = response.data.config + const [ chg, def ] = [ response.data.config.userChangable, response.data.config.default ] + const len = response.data.fileLength - document.getElementById('setFileLength').addEventListener('click', function () { - page.setFileLength(document.getElementById('fileLength').value, this) - }) - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + page.dom.innerHTML = ` +

File name length

+
+
+ +
+ +
+

Default file name length is ${def} characters. ${(chg ? `Range allowed for user is ${min} to ${max} characters.` : 'Changing file name length is disabled at the moment.')}

+
+
+ +
+
+ ` + + document.getElementById('setFileLength').addEventListener('click', function () { + page.setFileLength(document.getElementById('fileLength').value, this) }) + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.setFileLength = function (fileLength, element) { page.isLoading(element, true) - axios.post('api/filelength/change', { fileLength: fileLength }) - .then(function (response) { - page.isLoading(element, false) + axios.post('api/filelength/change', { fileLength }).then(function (response) { + page.isLoading(element, false) - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - swal({ - title: 'Woohoo!', - text: 'Your file length was successfully changed.', - icon: 'success' - }) - .then(function () { - page.changeFileLength() - }) - }) - .catch(function (error) { - console.log(error) - page.isLoading(element, false) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + swal({ + title: 'Woohoo!', + text: 'Your file length was successfully changed.', + icon: 'success' + }).then(function () { + page.changeFileLength() }) + }).catch(function (error) { + console.log(error) + page.isLoading(element, false) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.changeToken = function () { - axios.get('api/tokens') - .then(function (response) { - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + axios.get('api/tokens').then(function (response) { + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - page.dom.innerHTML = - '

Manage your token

\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '' - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + page.dom.innerHTML = ` +

Manage your token

+
+ +
+
+ +
+
+
+ + ` + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.getNewToken = function (element) { page.isLoading(element, true) - axios.post('api/tokens/change') - .then(function (response) { - page.isLoading(element, false) + axios.post('api/tokens/change').then(function (response) { + page.isLoading(element, false) - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - swal({ - title: 'Woohoo!', - text: 'Your token was successfully changed.', - icon: 'success' - }) - .then(function () { - axios.defaults.headers.common.token = response.data.token - localStorage.token = response.data.token - page.token = response.data.token - page.changeToken() - }) - }) - .catch(function (error) { - console.log(error) - page.isLoading(element, false) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + swal({ + title: 'Woohoo!', + text: 'Your token was successfully changed.', + icon: 'success' + }).then(function () { + axios.defaults.headers.common.token = response.data.token + localStorage[LS_KEYS.token] = response.data.token + page.token = response.data.token + page.changeToken() }) + }).catch(function (error) { + console.log(error) + page.isLoading(element, false) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.changePassword = function () { - page.dom.innerHTML = - '

Change your password

\n' + - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '' + page.dom.innerHTML = ` +

Change your password

+
+ +
+ +
+
+
+ +
+ +
+
+ + ` document.getElementById('sendChangePassword').addEventListener('click', function () { if (document.getElementById('password').value === document.getElementById('passwordConfirm').value) { @@ -1361,38 +1439,35 @@ page.changePassword = function () { page.sendNewPassword = function (pass, element) { page.isLoading(element, true) - axios.post('api/password/change', { password: pass }) - .then(function (response) { - page.isLoading(element, false) + axios.post('api/password/change', { password: pass }).then(function (response) { + page.isLoading(element, false) - if (response.data.success === false) { - if (response.data.description === 'No token provided') { - return page.verifyToken(page.token) - } else { - return swal('An error occurred!', response.data.description, 'error') - } + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') } + } - swal({ - title: 'Woohoo!', - text: 'Your password was successfully changed.', - icon: 'success' - }) - .then(function () { - page.changePassword() - }) - }) - .catch(function (error) { - console.log(error) - page.isLoading(element, false) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + swal({ + title: 'Woohoo!', + text: 'Your password was successfully changed.', + icon: 'success' + }).then(function () { + page.changePassword() }) + }).catch(function (error) { + console.log(error) + page.isLoading(element, false) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.setActiveMenu = function (activeItem) { - var menu = document.getElementById('menu') - var items = menu.getElementsByTagName('a') - for (var i = 0; i < items.length; i++) { + const menu = document.getElementById('menu') + const items = menu.getElementsByTagName('a') + for (let i = 0; i < items.length; i++) { items[i].classList.remove('is-active') } @@ -1420,24 +1495,297 @@ page.getPrettyBytes = num => { const neg = num < 0 if (neg) { num = -num } - if (num < 1) { return (neg ? '-' : '') + num + ' B' } + if (num < 1) { return `${neg ? '-' : ''}${num}B` } const exponent = Math.min(Math.floor(Math.log10(num) / 3), page.byteUnits.length - 1) const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3)) const unit = page.byteUnits[exponent] - return (neg ? '-' : '') + numStr + ' ' + unit + return `${neg ? '-' : ''}${numStr} ${unit}` } +page.getUsers = ({ pageNum } = {}, element) => { + if (element) { page.isLoading(element, true) } + if (pageNum === undefined) { pageNum = 0 } + + if (!page.permissions.admin) { + return swal('An error occurred!', 'You can not do this!', 'error') + } + + const url = `api/users/${pageNum}` + console.log(url) + axios.get(url).then(function (response) { + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } + + if (pageNum && (response.data.users.length === 0)) { + // Only remove loading class here, since beyond this the entire page will be replaced anyways + if (element) { page.isLoading(element, false) } + return swal('An error occurred!', 'There are no more users!', 'error') + } + + console.log(response.data.users) + + page.currentView = 'users' + page.cache.users = {} + + const pagination = ` + + ` + + const controls = ` + + ` + + let allSelected = true + + page.dom.innerHTML = ` + ${pagination} +
+ ${controls} +
+ + + + + + + + + + + + +
UsernameFile lengthGroup
+
+
+ ${pagination} + ` + + const table = document.getElementById('table') + + for (let i = 0; i < response.data.users.length; i++) { + const user = response.data.users[i] + const selected = page.selected.users.includes(user.id) + if (!selected && allSelected) { allSelected = false } + + page.cache.users[user.id] = { + username: user.username, + groups: user.groups, + enabled: user.enabled + } + + let group = 'User' + for (const g of Object.keys(user.groups)) { + if (!user.groups[g]) { break } + group = g + } + + const tr = document.createElement('tr') + tr.dataset.id = user.id + tr.innerHTML = ` + + ${user.username} + ${user.fileLength || 'default'} + ${group} + + + + + + + + + + + + + + + + + + ` + + table.appendChild(tr) + // page.checkboxes.users = Array.from(table.getElementsByClassName('checkbox')) + page.checkboxes.users = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) + } + + if (allSelected && response.data.users.length) { + const selectAll = document.getElementById('selectAll') + if (selectAll) { selectAll.checked = true } + } + + page.views.users.pageNum = response.data.users.length ? pageNum : 0 + }).catch(function (error) { + if (element) { page.isLoading(element, false) } + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) +} + +page.editUser = function (id) { + const user = page.cache.users[id] + if (!user) { return } + + const div = document.createElement('div') + div.innerHTML = ` +
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+
+ ` + + swal({ + title: 'Edit user', + icon: 'info', + content: div, + buttons: { + cancel: true, + confirm: { + closeModal: false + } + } + }).then(function (value) { + if (!value) { return } + + axios.post('api/users/edit', { + id, + username: document.getElementById('swalUsername').value, + enabled: document.getElementById('swalEnabled').checked, + resetPassword: document.getElementById('swalResetPassword').checked + }).then(function (response) { + if (!response) { return } + + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } + + if (response.data.password) { + const div = document.createElement('div') + div.innerHTML = ` +

${user.username}'s new password is:

+

${response.data.password}

+ ` + swal({ + title: 'Success!', + icon: 'success', + content: div + }) + } else if (response.data.name !== user.name) { + swal('Success!', `${user.username} was renamed into: ${response.data.name}.`, 'success') + } else { + swal('Success!', 'The user was edited!', 'success') + } + + page.getUsers(page.views.users) + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) + }) +} + +/* +page.disableUser = function (id) { + swal({ + title: 'Are you sure?', + text: `You will be disabling a user with the username ${page.cache.users.id.username}!`, + icon: 'warning', + dangerMode: true, + buttons: { + cancel: true, + confirm: { + text: 'Yes, disable them!', + closeModal: false + } + } + }).then(function (proceed) { + if (!proceed) { return } + + axios.post('api/users/disable', { id }).then(function (response) { + if (!response) { return } + + if (response.data.success === false) { + if (response.data.description === 'No token provided') { + return page.verifyToken(page.token) + } else { + return swal('An error occurred!', response.data.description, 'error') + } + } + + swal('Success!', 'The user has been disabled.', 'success') + page.getUsers(page.views.users) + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) + }) +} +*/ + window.onload = function () { // Add 'no-touch' class to non-touch devices if (!('ontouchstart' in document.documentElement)) { document.documentElement.classList.add('no-touch') } - var selectedFiles = localStorage.selectedFiles - if (selectedFiles) { - page.selectedFiles = JSON.parse(selectedFiles) + const selectedKeys = ['uploads', 'users'] + for (const selectedKey of selectedKeys) { + const ls = localStorage[LS_KEYS.selected[selectedKey]] + if (ls) { page.selected[selectedKey] = JSON.parse(ls) } } page.preparePage() diff --git a/public/js/home.js b/public/js/home.js index cc5e9ea..db61ebc 100644 --- a/public/js/home.js +++ b/public/js/home.js @@ -1,6 +1,6 @@ /* global swal, axios, Dropzone, ClipboardJS, LazyLoad */ -var page = { +const page = { // user token token: localStorage.token, @@ -21,24 +21,22 @@ var page = { lazyLoad: null } -var imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png'] +const imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png'] page.checkIfPublic = function () { - axios.get('api/check') - .then(function (response) { - page.private = response.data.private - page.enableUserAccounts = response.data.enableUserAccounts - page.maxFileSize = response.data.maxFileSize - page.chunkSize = response.data.chunkSize - page.preparePage() - }) - .catch(function (error) { - console.log(error) - var button = document.getElementById('loginToUpload') - button.classList.remove('is-loading') - button.innerText = 'Error occurred. Reload the page?' - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + axios.get('api/check').then(function (response) { + page.private = response.data.private + page.enableUserAccounts = response.data.enableUserAccounts + page.maxFileSize = response.data.maxFileSize + page.chunkSize = response.data.chunkSize + page.preparePage() + }).catch(function (error) { + console.log(error) + const button = document.getElementById('loginToUpload') + button.classList.remove('is-loading') + button.innerText = 'Error occurred. Reload the page?' + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.preparePage = function () { @@ -46,7 +44,7 @@ page.preparePage = function () { if (page.token) { return page.verifyToken(page.token, true) } else { - var button = document.getElementById('loginToUpload') + const button = document.getElementById('loginToUpload') button.href = 'auth' button.classList.remove('is-loading') @@ -64,29 +62,26 @@ page.preparePage = function () { page.verifyToken = function (token, reloadOnError) { if (reloadOnError === undefined) { reloadOnError = false } - axios.post('api/tokens/verify', { token: token }) - .then(function (response) { - if (response.data.success === false) { - return swal({ - title: 'An error occurred!', - text: response.data.description, - icon: 'error' - }) - .then(function () { - if (!reloadOnError) { return } - localStorage.removeItem('token') - location.reload() - }) - } + axios.post('api/tokens/verify', { token }).then(function (response) { + if (response.data.success === false) { + return swal({ + title: 'An error occurred!', + text: response.data.description, + icon: 'error' + }).then(function () { + if (!reloadOnError) { return } + localStorage.removeItem('token') + location.reload() + }) + } - localStorage.token = token - page.token = token - return page.prepareUpload() - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + localStorage.token = token + page.token = token + return page.prepareUpload() + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.prepareUpload = function () { @@ -104,24 +99,24 @@ page.prepareUpload = function () { document.getElementById('albumDiv').style.display = 'flex' } - document.getElementById('maxFileSize').innerHTML = 'Maximum upload size per file is ' + page.maxFileSize + document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${page.maxFileSize}` document.getElementById('loginToUpload').style.display = 'none' if (!page.token && page.enableUserAccounts) { document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads' } - var previewNode = document.querySelector('#tpl') + const previewNode = document.querySelector('#tpl') page.previewTemplate = previewNode.innerHTML previewNode.parentNode.removeChild(previewNode) page.prepareDropzone() - var tabs = document.getElementById('tabs') + const tabs = document.getElementById('tabs') if (tabs) { tabs.style.display = 'flex' - var items = tabs.getElementsByTagName('li') - for (var i = 0; i < items.length; i++) { + const items = tabs.getElementsByTagName('li') + for (let i = 0; i < items.length; i++) { items[i].addEventListener('click', function () { page.setActiveTab(this.dataset.id) }) @@ -136,42 +131,44 @@ page.prepareUpload = function () { } page.prepareAlbums = function () { - var option = document.createElement('option') + const option = document.createElement('option') option.value = '' option.innerHTML = 'Upload to album' option.disabled = true option.selected = true page.albumSelect.appendChild(option) - axios.get('api/albums', { headers: { token: page.token } }) - .then(function (response) { - if (response.data.success === false) { - return swal('An error occurred!', response.data.description, 'error') - } + axios.get('api/albums', { + headers: { + token: page.token + } + }).then(function (response) { + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } - // If the user doesn't have any albums we don't really need to display - // an album selection - if (!response.data.albums.length) { return } + // If the user doesn't have any albums we don't really need to display + // an album selection + if (!response.data.albums.length) { return } - // Loop through the albums and create an option for each album - for (var i = 0; i < response.data.albums.length; i++) { - var album = response.data.albums[i] - var option = document.createElement('option') - option.value = album.id - option.innerHTML = album.name - page.albumSelect.appendChild(option) - } - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + // Loop through the albums and create an option for each album + for (let i = 0; i < response.data.albums.length; i++) { + const album = response.data.albums[i] + const option = document.createElement('option') + option.value = album.id + option.innerHTML = album.name + page.albumSelect.appendChild(option) + } + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') + }) } page.setActiveTab = function (activeId) { - var items = document.getElementById('tabs').getElementsByTagName('li') - for (var i = 0; i < items.length; i++) { - var tabId = items[i].dataset.id + const items = document.getElementById('tabs').getElementsByTagName('li') + for (let i = 0; i < items.length; i++) { + const tabId = items[i].dataset.id if (tabId === activeId) { items[i].classList.add('is-active') document.getElementById(tabId).style.display = 'block' @@ -183,27 +180,28 @@ page.setActiveTab = function (activeId) { } page.prepareDropzone = function () { - var tabDiv = document.getElementById('tab-files') - var div = document.createElement('div') + const tabDiv = document.getElementById('tab-files') + const div = document.createElement('div') div.className = 'control is-expanded' - div.innerHTML = - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' Click here or drag and drop files\n' + - '
' + div.innerHTML = ` +
+ + + + Click here or drag and drop files +
+ ` tabDiv.getElementsByClassName('dz-container')[0].appendChild(div) - var previewsContainer = tabDiv.getElementsByClassName('uploads')[0] + const previewsContainer = tabDiv.getElementsByClassName('uploads')[0] page.dropzone = new Dropzone('#dropzone', { url: 'api/upload', paramName: 'files[]', maxFilesize: parseInt(page.maxFileSize), parallelUploads: 2, uploadMultiple: false, - previewsContainer: previewsContainer, + previewsContainer, previewTemplate: page.previewTemplate, createImageThumbnails: false, maxFiles: 1000, @@ -212,45 +210,41 @@ page.prepareDropzone = function () { chunking: Boolean(page.chunkSize), chunkSize: parseInt(page.chunkSize) * 1000000, // 1000000 B = 1 MB, parallelChunkUploads: false, // when set to true, sometimes it often hangs with hundreds of parallel uploads - chunksUploaded: function (file, done) { + chunksUploaded (file, done) { file.previewElement.querySelector('.progress').setAttribute('value', 100) file.previewElement.querySelector('.progress').innerHTML = '100%' - // The API supports an array of multiple files - return axios.post('api/upload/finishchunks', - { - files: [{ - uuid: file.upload.uuid, - original: file.name, - size: file.size, - type: file.type, - count: file.upload.totalChunkCount, - albumid: page.album - }] - }, - { - headers: { - token: page.token - } - }) - .then(function (response) { - file.previewElement.querySelector('.progress').style.display = 'none' + return axios.post('api/upload/finishchunks', { + // The API supports an array of multiple files + files: [{ + uuid: file.upload.uuid, + original: file.name, + size: file.size, + type: file.type, + count: file.upload.totalChunkCount, + albumid: page.album + }] + }, { + headers: { + token: page.token + } + }).then(function (response) { + file.previewElement.querySelector('.progress').style.display = 'none' - if (response.data.success === false) { - file.previewElement.querySelector('.error').innerHTML = response.data.description - } + if (response.data.success === false) { + file.previewElement.querySelector('.error').innerHTML = response.data.description + } - if (response.data.files && response.data.files[0]) { - page.updateTemplate(file, response.data.files[0]) - } - return done() - }) - .catch(function (error) { - return { - success: false, - description: error.toString() - } - }) + if (response.data.files && response.data.files[0]) { + page.updateTemplate(file, response.data.files[0]) + } + return done() + }).catch(function (error) { + return { + success: false, + description: error.toString() + } + }) } }) @@ -269,7 +263,7 @@ page.prepareDropzone = function () { page.dropzone.on('uploadprogress', function (file, progress) { if (file.upload.chunked && progress === 100) { return } file.previewElement.querySelector('.progress').setAttribute('value', progress) - file.previewElement.querySelector('.progress').innerHTML = progress + '%' + file.previewElement.querySelector('.progress').innerHTML = `${progress}%` }) page.dropzone.on('success', function (file, response) { @@ -296,7 +290,7 @@ page.prepareDropzone = function () { } page.uploadUrls = function (button) { - var tabDiv = document.getElementById('tab-urls') + const tabDiv = document.getElementById('tab-urls') if (!tabDiv) { return } if (button.classList.contains('is-loading')) { return } @@ -308,9 +302,9 @@ page.uploadUrls = function (button) { } function run () { - var albumid = page.album - var previewsContainer = tabDiv.getElementsByClassName('uploads')[0] - var urls = document.getElementById('urls').value + const albumid = page.album + const previewsContainer = tabDiv.getElementsByClassName('uploads')[0] + const urls = document.getElementById('urls').value .split(/\r?\n/) .filter(function (url) { return url.trim().length }) document.getElementById('urls').value = urls.join('\n') @@ -321,22 +315,22 @@ page.uploadUrls = function (button) { } tabDiv.getElementsByClassName('uploads')[0].style.display = 'block' - var files = urls.map(function (url) { - var previewTemplate = document.createElement('template') + const files = urls.map(function (url) { + const previewTemplate = document.createElement('template') previewTemplate.innerHTML = page.previewTemplate.trim() - var previewElement = previewTemplate.content.firstChild + const previewElement = previewTemplate.content.firstChild previewElement.querySelector('.name').innerHTML = url previewsContainer.appendChild(previewElement) return { - url: url, - previewElement: previewElement + url, + previewElement } }) function post (i) { if (i === files.length) { return done() } - var file = files[i] + const file = files[i] function posted (result) { file.previewElement.querySelector('.progress').style.display = 'none' @@ -348,25 +342,21 @@ page.uploadUrls = function (button) { return post(i + 1) } - axios.post('api/upload', - { - urls: [file.url] - }, - { - headers: { - token: page.token, - albumid: albumid - } - }) - .then(function (response) { - return posted(response.data) - }) - .catch(function (error) { - return posted({ - success: false, - description: error.toString() - }) + axios.post('api/upload', { + urls: [file.url] + }, { + headers: { + token: page.token, + albumid + } + }).then(function (response) { + return posted(response.data) + }).catch(function (error) { + return posted({ + success: false, + description: error.toString() }) + }) } return post(0) } @@ -376,14 +366,14 @@ page.uploadUrls = function (button) { page.updateTemplate = function (file, response) { if (!response.url) { return } - var a = file.previewElement.querySelector('.link > a') - var clipboard = file.previewElement.querySelector('.clipboard-mobile > .clipboard-js') + const a = file.previewElement.querySelector('.link > a') + const clipboard = file.previewElement.querySelector('.clipboard-mobile > .clipboard-js') a.href = a.innerHTML = clipboard.dataset['clipboardText'] = response.url clipboard.parentElement.style.display = 'block' - var exec = /.[\w]+(\?|$)/.exec(response.url) + const exec = /.[\w]+(\?|$)/.exec(response.url) if (exec && exec[0] && imageExtensions.includes(exec[0].toLowerCase())) { - var img = file.previewElement.querySelector('img') + const img = file.previewElement.querySelector('img') img.setAttribute('alt', response.name || '') img.dataset['src'] = response.url img.onerror = function () { this.style.display = 'none' } // hide webp in firefox and ie @@ -392,30 +382,31 @@ page.updateTemplate = function (file, response) { } page.createAlbum = function () { - var div = document.createElement('div') - div.innerHTML = - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '
' + const div = document.createElement('div') + div.innerHTML = ` +
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+
+ ` swal({ title: 'Create new album', @@ -427,43 +418,44 @@ page.createAlbum = function () { closeModal: false } } - }) - .then(function (value) { - if (!value) { return } + }).then(function (value) { + if (!value) { return } - var name = document.getElementById('_name').value - axios.post('api/albums', { - name: name, - download: document.getElementById('_download').checked, - public: document.getElementById('_public').checked - }, { headers: { token: page.token } }) - .then(function (response) { - if (response.data.success === false) { - return swal('An error occurred!', response.data.description, 'error') - } + const name = document.getElementById('swalName').value + axios.post('api/albums', { + name, + download: document.getElementById('swalDownload').checked, + public: document.getElementById('swalPublic').checked + }, { + headers: { + token: page.token + } + }).then(function (response) { + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } - var option = document.createElement('option') - option.value = response.data.id - option.innerHTML = name - page.albumSelect.appendChild(option) + const option = document.createElement('option') + option.value = response.data.id + option.innerHTML = name + page.albumSelect.appendChild(option) - swal('Woohoo!', 'Album was created successfully', 'success') - }) - .catch(function (error) { - console.log(error) - return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') - }) + swal('Woohoo!', 'Album was created successfully', 'success') + }).catch(function (error) { + console.log(error) + return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) + }) } // Handle image paste event window.addEventListener('paste', function (event) { - var items = (event.clipboardData || event.originalEvent.clipboardData).items - for (var index in items) { - var item = items[index] + const items = (event.clipboardData || event.originalEvent.clipboardData).items + for (const index in items) { + const item = items[index] if (item.kind === 'file') { - var blob = item.getAsFile() - var file = new File([blob], 'pasted-image.' + blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]) + const blob = item.getAsFile() + const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`) file.type = blob.type page.dropzone.addFile(file) } diff --git a/public/js/sharex.js b/public/js/sharex.js index 239cfaf..da42bfc 100644 --- a/public/js/sharex.js +++ b/public/js/sharex.js @@ -2,24 +2,23 @@ page.prepareShareX = function () { if (!page.token) { return } - var origin = (location.hostname + location.pathname).replace(/\/(dashboard)?$/, '') - var originClean = origin.replace(/\//g, '_') - var sharexElement = document.getElementById('ShareX') - var sharexFile = - '{\r\n' + - ' "Name": "' + originClean + '",\r\n' + - ' "DestinationType": "ImageUploader, FileUploader",\r\n' + - ' "RequestType": "POST",\r\n' + - ' "RequestURL": "' + location.protocol + '//' + origin + '/api/upload",\r\n' + - ' "FileFormName": "files[]",\r\n' + - ' "Headers": {\r\n' + - ' "token": "' + page.token + '"\r\n' + - ' },\r\n' + - ' "ResponseType": "Text",\r\n' + - ' "URL": "$json:files[0].url$",\r\n' + - ' "ThumbnailURL": "$json:files[0].url$"\r\n' + - '}' - var sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' }) + const origin = (location.hostname + location.pathname).replace(/\/(dashboard)?$/, '') + const originClean = origin.replace(/\//g, '_') + const sharexElement = document.getElementById('ShareX') + const sharexFile = `{ + "Name": "${originClean}", + "DestinationType": "ImageUploader, FileUploader", + "RequestType": "POST", + "RequestURL": "${location.protocol}//${origin}/api/upload", + "FileFormName": "files[]", + "Headers": { + "token": "${page.token}" + }, + "ResponseType": "Text", + "URL": "$json:files[0].url$", + "ThumbnailURL": "$json:files[0].url$" +}\n` + const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' }) sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob)) - sharexElement.setAttribute('download', originClean + '.sxcu') + sharexElement.setAttribute('download', `${originClean}.sxcu`) } diff --git a/public/libs/fontello/fontello.css b/public/libs/fontello/fontello.css index c9367ca..356172f 100644 --- a/public/libs/fontello/fontello.css +++ b/public/libs/fontello/fontello.css @@ -1,11 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('fontello.eot?61363773'); - src: url('fontello.eot?61363773#iefix') format('embedded-opentype'), - url('fontello.woff2?61363773') format('woff2'), - url('fontello.woff?61363773') format('woff'), - url('fontello.ttf?61363773') format('truetype'), - url('fontello.svg?61363773#fontello') format('svg'); + src: url('fontello.eot?54767678'); + src: url('fontello.eot?54767678#iefix') format('embedded-opentype'), + url('fontello.woff2?54767678') format('woff2'), + url('fontello.woff?54767678') format('woff'), + url('fontello.ttf?54767678') format('truetype'), + url('fontello.svg?54767678#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -33,6 +33,10 @@ text-align: center; /* opacity: .8; */ + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + /* fix buttons height, for twitter bootstrap */ /* line-height: 1em; */ @@ -55,12 +59,12 @@ font-size: 2rem; } -.icon-sharex:before { content: '\e800'; } /* '' */ -.icon-upload-cloud:before { content: '\e801'; } /* '' */ -.icon-picture-1:before { content: '\e802'; } /* '' */ -.icon-th-list:before { content: '\e803'; } /* '' */ -.icon-trash:before { content: '\e804'; } /* '' */ -.icon-pencil-1:before { content: '\e805'; } /* '' */ +.icon-pencil-1:before { content: '\e800'; } /* '' */ +.icon-sharex:before { content: '\e801'; } /* '' */ +.icon-upload-cloud:before { content: '\e802'; } /* '' */ +.icon-picture-1:before { content: '\e803'; } /* '' */ +.icon-th-list:before { content: '\e804'; } /* '' */ +.icon-trash:before { content: '\e805'; } /* '' */ .icon-th-large:before { content: '\e806'; } /* '' */ .icon-arrows-cw:before { content: '\e807'; } /* '' */ .icon-plus:before { content: '\e808'; } /* '' */ @@ -72,6 +76,7 @@ .icon-download:before { content: '\e80e'; } /* '' */ .icon-help-circled:before { content: '\e80f'; } /* '' */ .icon-terminal:before { content: '\e810'; } /* '' */ +.icon-hammer:before { content: '\e811'; } /* '' */ .icon-github-circled:before { content: '\f09b'; } /* '' */ .icon-gauge:before { content: '\f0e4'; } /* '' */ .icon-paper-plane-empty:before { content: '\f1d9'; } /* '' */ diff --git a/public/libs/fontello/fontello.eot b/public/libs/fontello/fontello.eot index 3c6b03f68d35ae1c517a9a2b8aaf71ec089ce1c2..4ad422760c29f1ad2e36f43bf710e085e65e5ea1 100644 GIT binary patch delta 1067 zcma)4U1%I-6usZieDm$hIJ3K%+1X8-{nd>nrb*+vDQ#m{D`~aRmI@`UQM;s@Y-~0& zw4uZztcZeCvBvRDDgmXGJ{1;x5PxbY1#PUp1*L_yMhNt!7A-A^@lIC3M`t*kIp?0a z_s-9y(fPw7+Yd0?Hz&$`DRcY5T;-kS*8pT602~-=RQ*p5op}!RIRN_e*om2}_~oyA z0ENR?&yM>OjZ<^)A4L5+fbLIJkNfCHQI8mm1YspX@S*P;W+^ zovt=&i)YeV)HslS?|a8*erR^g0OTLVrThJ3HUHPAdcFYIcqf=AFOZADpJZagc_4T< zz^B00t{y|yFn=C6%%X3C1PWp$7Q9P)BmMBt87$CEN)mG-Anbk+Wx8dAZ3w=>wIdlw zes%v!3?Kx=0q%U%26P}Vw*fZ5Z$GsG4#3?~8<0S*w*fBlWE+qS-qV6sPVtpim(H~& zF28z$U1LMw!P}M29fT{-4^hS^3p!JUMTq)}Mk!Ht?!M5uZ=|vT6s~x{8TWt*=7CU! z2P#oLAe4Ccq}0Q0AA)DsRl5I2wu#~KgB5e?=u4w}_pIO8m$apl>QQWqhc4p~iYug{7 zvlTR9>DGsle>tdh4X#UtKD&-U=CRe)HGK*7=HP>_zTnHQOc{pBe)1WO(?tesnsZ*_ zBMK;AiCyA?T2eokRNjjGsJ)?G)lV7~kZz@jjEzvZ8U1ff}6P=tw-`1<8OGf&@cc1 delta 572 zcmXw!PiPZC7{%Z0>~1!Huu*Fhu$t9GMC`^yLcAzg6zfSmNU>EhYnmp7Y`U~-sVI06 z59*;|5WzoCs$h{Gq`8!$IY=);BGQZW;7KF7Sel3)MAR=`o#D-!-y7zeZ)P(q7uAz- z;IUCw({g6D-Y6fxd-FBG&H@LE;a8Y<=!L0D6g$E>Pfk%fAsX5XTCZGd;96 z_@_<5dkQke@vJ$p-oFIwN)R8m%xRkq1db7p5bGt=%GI9i)`_Vhscw%?I$sx^eROn= zn%-|uFMEoRvTRu#Wk{=hsK-3I2sH+OyHzRgd{n4)|OG)PdJL2*wqym9=S z;*FG~3#wrJrwDRySD;gb#X2jEknqXxPHj8_I>I*c&#tf?Xsx+IH_%SG!VdC+EA)_$ zxk9M2q+C^mq0Br@`hAsF57&Ad_0`PfuA|@K=grP@AAI5K|6K$lM|hYw#hFNmeH2$B zhs6EJ&NK@!o7JS-(lQ4x$+Dc1&nr->>S^_*C*@hzByGbx=Uw+b^k>|^P{DBJOT^Dv fShV*h7(_g3Xm+lY9V;41F&w=ls?igRF+K1XGK`Ws diff --git a/public/libs/fontello/fontello.svg b/public/libs/fontello/fontello.svg index c58404f..8b3a964 100644 --- a/public/libs/fontello/fontello.svg +++ b/public/libs/fontello/fontello.svg @@ -6,17 +6,17 @@ - + - + - + - + - + - + @@ -40,6 +40,8 @@ + + diff --git a/public/libs/fontello/fontello.ttf b/public/libs/fontello/fontello.ttf index 93ed878cfe26e620c3e19dd68a679f9b7cbf7e0e..cf147ab4fff93c97a98453c9957518726fd7a599 100644 GIT binary patch delta 1029 zcma))UuYCp6vof}Gk5OHPG_>o%+791vg@vFDlsO;bYs=%S|zp$YPCrG?-DoJXx14t z4N+RQls*(+RHhFFO(|Gx@kv1btCa(HGM**$O_o!#8KX@BZeT zyEAio$E)2ZGL=67$R+^TI#4e7XSPqQL;o89y)!U2niBuMc?po&i|eUDf2e%yL4{}D9OMM@kwaprSM>Jc=;E~GO&<1ZfUWw$m@M*%_s0#8oW1PxfH zY62VJ>EAVh1MsZ7CWv7z)dViq;hM1RLoKKt;l5J+Qs;UF_dYYm?y_F+;QM^zYQmK* zy_E6cOoXYzB1GM(4iC>d(&yg)1I##ywzyc_38bfl5>l2qhl=QR?AtKY-8X zrvfNKp)lj4!(JVnF*D3bDETyRmoK2+@ zMcky4glI_C@p?;A!$b@RIM?wlLv`Hdvg!5X#v|+58d vbA*vZFqY~xNFzwUcyM5Nq*c#WQB4VSdgz7x z?~YFF)ZPM$3mCBT)=1ME_bJduj4qpk;3xSf@d9zX?YPqet9|ud3cgT~Esp1`o~`cp zz>zfZS;v~@tS54jc!bz2Sx&z8GHw!6L(&Q#pL91D-Q#rBOHH5P6M4Sged+^{YM$L2 zWJ{u#6;95B(Pdx`a%0y)t~dUuV2bj6&>%tGh>A6q3Z2HkDGp1WbV04$lq42K#Cw9Z zdn=6=aa;a;W&1hc2?xm6eBmIlv+4`Yz;4DD4w2iw5F;P+g_cS*a3>%hDdks>b-i*e zCTnf$D_^t2`!D=LSl;l;ebbG5|6SPebFGG#ds3R}^~Qdg_&n*msK(ET&3J1Ui!jb= z(u`DbqbjSk)wLJ2V&C(w~L1{_m)SQMq(9EHgt~Pox>dOd_T6 Wd?`0pOm&LI#4Qm^Uas^f&B$Nm4UVJ$ diff --git a/public/libs/fontello/fontello.woff b/public/libs/fontello/fontello.woff index 7d99ec4f97cfe9c690fd3c411c86a7ce858610f8..4e86699f656624a056ae769edeb7b9909432930a 100644 GIT binary patch delta 5650 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 diff --git a/routes/api.js b/routes/api.js index 3f867b5..6124077 100644 --- a/routes/api.js +++ b/routes/api.js @@ -41,5 +41,8 @@ routes.post('/tokens/verify', (req, res, next) => tokenController.verify(req, re routes.post('/tokens/change', (req, res, next) => tokenController.change(req, res, next)) routes.get('/filelength/config', (req, res, next) => authController.getFileLengthConfig(req, res, next)) routes.post('/filelength/change', (req, res, next) => authController.changeFileLength(req, res, next)) +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)) module.exports = routes diff --git a/views/dashboard.njk b/views/dashboard.njk index 2125c19..9c87b3d 100644 --- a/views/dashboard.njk +++ b/views/dashboard.njk @@ -70,6 +70,15 @@ + +