Init account-manager branch

This commit is contained in:
Bobby Wibowo 2018-10-10 02:52:41 +07:00
parent 0745456cac
commit c3d4c237cb
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
24 changed files with 1762 additions and 1184 deletions

View File

@ -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.' })

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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) => {

View File

@ -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(() => {})
})
})

View File

@ -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)
}

View File

@ -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()

View File

@ -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
}

View File

@ -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;

View File

@ -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
}
]
}
}

View File

@ -1,6 +1,6 @@
/* global LazyLoad */
var page = {}
const page = {}
window.onload = function () {
page.lazyLoad = new LazyLoad()

View File

@ -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 () {

File diff suppressed because it is too large Load Diff

View File

@ -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 =
'<div id="dropzone" class="button is-danger is-fullwidth is-unselectable">\n' +
' <span class="icon">\n' +
' <i class="icon-upload-cloud"></i>\n' +
' </span>\n' +
' <span>Click here or drag and drop files</span>\n' +
'</div>'
div.innerHTML = `
<div id="dropzone" class="button is-danger is-fullwidth is-unselectable">
<span class="icon">
<i class="icon-upload-cloud"></i>
</span>
<span>Click here or drag and drop files</span>
</div>
`
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 =
'<div class="field">\n' +
' <label class="label">Album name</label>\n' +
' <div class="controls">\n' +
' <input id="_name" class="input" type="text" placeholder="My super album">\n' +
' </div>\n' +
'</div>\n' +
'<div class="field">\n' +
' <div class="control">\n' +
' <label class="checkbox">\n' +
' <input id="_download" type="checkbox" checked>\n' +
' Enable download\n' +
' </label>\n' +
' </div>\n' +
'</div>\n' +
'<div class="field">\n' +
' <div class="control">\n' +
' <label class="checkbox">\n' +
' <input id="_public" type="checkbox" checked>\n' +
' Enable public link\n' +
' </label>\n' +
' </div>\n' +
'</div>'
const div = document.createElement('div')
div.innerHTML = `
<div class="field">
<label class="label">Album name</label>
<div class="controls">
<input id="swalName" class="input" type="text" placeholder="My super album">
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input id="swalDownload" type="checkbox" checked>
Enable download
</label>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input id="swalPublic" type="checkbox" checked>
Enable public link
</label>
</div>
</div>
`
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)
}

View File

@ -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`)
}

View File

@ -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'; } /* '' */

Binary file not shown.

View File

@ -6,17 +6,17 @@
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="sharex" unicode="&#xe800;" d="M328-73c-98 10-183 74-229 171-52 110-43 246 24 346 44 66 107 110 181 126 21 4 70 4 91 0 45-10 86-30 120-59 7-5 12-10 12-10-1 0-10-2-20-4-122-17-223-98-257-205-10-29-12-45-12-79 0-34 2-50 12-80 14-41 35-75 70-109 32-33 64-55 107-72l16-7-7-3c-12-5-45-13-65-15-20-2-25-2-43 0z m248-11c-64 5-121 24-170 56-66 44-110 108-126 182-4 21-4 70 1 91 9 45 29 86 58 120 5 7 10 12 10 12 1-1 2-10 4-20 17-122 98-223 205-257 29-10 45-12 79-12 46 0 74 7 117 28 62 31 115 90 144 161l7 16 3-7c10-23 18-74 16-99-8-103-72-190-172-238-53-25-120-38-176-33z m42 212c-45 7-90 27-127 55-20 16-20 17-9 18 129 16 233 97 268 207 10 29 12 45 12 79 0 46-7 74-28 117-30 61-88 114-157 142-13 6-19 9-17 10 4 3 35 12 50 14 24 5 42 5 62 3 65-7 122-35 170-85 24-25 39-47 55-78 22-43 33-84 37-134 6-77-15-157-56-220-44-65-107-109-180-125-15-4-64-5-80-3z m31 204c-16 129-97 233-207 268-29 10-45 12-79 12-44 0-72-7-112-25-30-15-51-30-77-56-33-33-55-65-72-108l-7-16-3 7c-10 23-18 74-16 99 9 103 72 190 172 238 81 39 178 44 264 16 38-12 67-27 98-50 55-42 95-103 109-169 4-18 6-57 3-76-6-47-25-92-55-131-16-20-16-20-18-9z" horiz-adv-x="1000" />
<glyph glyph-name="pencil-1" unicode="&#xe800;" d="M0-143l68 343 274-273z m137 392l422 422 259-260-421-422z m531 494q2 39 31 69t69 31 66-25l131-131q25-26 24-66t-30-69-69-30-66 24l-131 131q-27 27-25 66z" horiz-adv-x="989" />
<glyph glyph-name="upload-cloud" unicode="&#xe801;" d="M781 506q108 0 184-76t76-184-76-184-184-77l-208 0 0 239 67-66q16-17 38-17 20 0 36 17 15 15 15 36t-15 36l-156 156q-14 14-37 15t-37-15l-156-156q-15-15-15-36t15-36q16-17 37-17 20 0 36 17l68 66 0-239-260 0q-86 0-148 61t-61 148q0 72 44 128t112 73l0 8q0 130 92 221t221 91q100 0 180-58t114-152q2 0 8 1t10 0z" horiz-adv-x="1041" />
<glyph glyph-name="sharex" unicode="&#xe801;" d="M328-73c-98 10-183 74-229 171-52 110-43 246 24 346 44 66 107 110 181 126 21 4 70 4 91 0 45-10 86-30 120-59 7-5 12-10 12-10-1 0-10-2-20-4-122-17-223-98-257-205-10-29-12-45-12-79 0-34 2-50 12-80 14-41 35-75 70-109 32-33 64-55 107-72l16-7-7-3c-12-5-45-13-65-15-20-2-25-2-43 0z m248-11c-64 5-121 24-170 56-66 44-110 108-126 182-4 21-4 70 1 91 9 45 29 86 58 120 5 7 10 12 10 12 1-1 2-10 4-20 17-122 98-223 205-257 29-10 45-12 79-12 46 0 74 7 117 28 62 31 115 90 144 161l7 16 3-7c10-23 18-74 16-99-8-103-72-190-172-238-53-25-120-38-176-33z m42 212c-45 7-90 27-127 55-20 16-20 17-9 18 129 16 233 97 268 207 10 29 12 45 12 79 0 46-7 74-28 117-30 61-88 114-157 142-13 6-19 9-17 10 4 3 35 12 50 14 24 5 42 5 62 3 65-7 122-35 170-85 24-25 39-47 55-78 22-43 33-84 37-134 6-77-15-157-56-220-44-65-107-109-180-125-15-4-64-5-80-3z m31 204c-16 129-97 233-207 268-29 10-45 12-79 12-44 0-72-7-112-25-30-15-51-30-77-56-33-33-55-65-72-108l-7-16-3 7c-10 23-18 74-16 99 9 103 72 190 172 238 81 39 178 44 264 16 38-12 67-27 98-50 55-42 95-103 109-169 4-18 6-57 3-76-6-47-25-92-55-131-16-20-16-20-18-9z" horiz-adv-x="1000" />
<glyph glyph-name="picture-1" unicode="&#xe802;" d="M0-68l0 836 1000 0 0-836-1000 0z m76 78l848 0 0 680-848 0 0-680z m90 80l0 59 150 195 102-86 193 291 223-228 0-231-668 0z m0 416q0 37 24 62t62 24q33 0 58-24t24-62q0-33-24-57t-58-25q-37 0-62 25t-24 57z" horiz-adv-x="1000" />
<glyph glyph-name="upload-cloud" unicode="&#xe802;" d="M781 506q108 0 184-76t76-184-76-184-184-77l-208 0 0 239 67-66q16-17 38-17 20 0 36 17 15 15 15 36t-15 36l-156 156q-14 14-37 15t-37-15l-156-156q-15-15-15-36t15-36q16-17 37-17 20 0 36 17l68 66 0-239-260 0q-86 0-148 61t-61 148q0 72 44 128t112 73l0 8q0 130 92 221t221 91q100 0 180-58t114-152q2 0 8 1t10 0z" horiz-adv-x="1041" />
<glyph glyph-name="th-list" unicode="&#xe803;" d="M860 90q43 0 73-31t31-74q0-43-31-73t-73-31l-365 0q-44 0-74 31t-31 73 31 74 74 31l365 0z m0 364q43 0 73-31t31-73-31-73-73-31l-365 0q-44 0-74 31t-31 73 31 73 74 31l365 0z m0 365q43 0 73-31t31-73q0-44-31-74t-73-31l-365 0q-42 0-74 31t-31 74 31 73 74 31l365 0z m-860-834q0 130 130 130t130-130-130-130-130 130z m0 365q0 130 130 130t130-130-130-130-130 130z m0 365q0 130 130 130t130-130-130-130-130 130z" horiz-adv-x="964" />
<glyph glyph-name="picture-1" unicode="&#xe803;" d="M0-68l0 836 1000 0 0-836-1000 0z m76 78l848 0 0 680-848 0 0-680z m90 80l0 59 150 195 102-86 193 291 223-228 0-231-668 0z m0 416q0 37 24 62t62 24q33 0 58-24t24-62q0-33-24-57t-58-25q-37 0-62 25t-24 57z" horiz-adv-x="1000" />
<glyph glyph-name="trash" unicode="&#xe804;" d="M0 633l0 141 289 0 0 76 246 0 0-76 289 0 0-141-824 0z m43-783l0 676 738 0 0-676-738 0z" horiz-adv-x="824" />
<glyph glyph-name="th-list" unicode="&#xe804;" d="M860 90q43 0 73-31t31-74q0-43-31-73t-73-31l-365 0q-44 0-74 31t-31 73 31 74 74 31l365 0z m0 364q43 0 73-31t31-73-31-73-73-31l-365 0q-44 0-74 31t-31 73 31 73 74 31l365 0z m0 365q43 0 73-31t31-73q0-44-31-74t-73-31l-365 0q-42 0-74 31t-31 74 31 73 74 31l365 0z m-860-834q0 130 130 130t130-130-130-130-130 130z m0 365q0 130 130 130t130-130-130-130-130 130z m0 365q0 130 130 130t130-130-130-130-130 130z" horiz-adv-x="964" />
<glyph glyph-name="pencil-1" unicode="&#xe805;" d="M0-143l68 343 274-273z m137 392l422 422 259-260-421-422z m531 494q2 39 31 69t69 31 66-25l131-131q25-26 24-66t-30-69-69-30-66 24l-131 131q-27 27-25 66z" horiz-adv-x="989" />
<glyph glyph-name="trash" unicode="&#xe805;" d="M0 633l0 141 289 0 0 76 246 0 0-76 289 0 0-141-824 0z m43-783l0 676 738 0 0-676-738 0z" horiz-adv-x="824" />
<glyph glyph-name="th-large" unicode="&#xe806;" d="M0 663q0 65 46 110t110 46l104 0q65 0 111-46t45-110l0-104q0-65-45-111t-111-45l-104 0q-65 0-110 45t-46 111l0 104z m521 0q0 65 46 110t111 46l103 0q65 0 111-46t46-110l0-104q0-65-46-111t-111-45l-103 0q-65 0-111 45t-46 111l0 104z m-521-521q0 65 46 110t110 46l104 0q65 0 111-46t45-110l0-104q0-65-45-111t-111-46l-104 0q-65 0-110 46t-46 111l0 104z m521 0q0 65 46 110t111 46l103 0q65 0 111-46t46-110l0-104q0-65-46-111t-111-46l-103 0q-65 0-111 46t-46 111l0 104z" horiz-adv-x="938" />
@ -40,6 +40,8 @@
<glyph glyph-name="terminal" unicode="&#xe810;" d="M929 779h-858c-39 0-71-32-71-72v-714c0-40 32-72 71-72h858c39 0 71 32 71 72v714c0 40-32 72-71 72z m-786-500l143 142-143 143 71 72 215-215-215-214-71 72z m571-72h-285v72h285v-72z" horiz-adv-x="1000" />
<glyph glyph-name="hammer" unicode="&#xe811;" d="M988-7q0-30-20-50l-60-61q-22-20-51-20-29 0-50 20l-203 204q-21 20-21 50 0 29 24 53l-143 143-70-70q-8-8-19-8t-19 8q1-1 7-7t7-7 6-6 5-8 4-8 3-9 0-10q0-21-15-38-2-1-9-10t-11-11-10-9-13-9-12-5-14-3q-23 0-38 16l-228 228q-16 15-16 38 0 7 3 14t5 12 9 13 9 10 11 11 10 9q17 15 38 15 6 0 10 0t9-3 8-4 8-5 6-6 7-7 7-7q-8 8-8 19t8 19l194 194q8 8 19 8t19-8q-1 1-7 7t-7 7-6 7-5 7-3 8-4 9 0 10q0 21 15 38 2 2 9 10t11 11 10 10 13 8 12 5 14 3q23 0 38-16l228-228q16-15 16-38 0-7-3-14t-5-12-8-13-10-10-11-11-10-9q-17-15-38-15-6 0-10 0t-9 4-8 3-7 5-7 6-7 7-7 7q8-8 8-19t-8-19l-70-70 143-143q24 24 53 24 29 0 51-21l203-202q20-22 20-51z" horiz-adv-x="1000" />
<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
<glyph glyph-name="gauge" unicode="&#xf0e4;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -70,6 +70,15 @@
</li>
</ul>
<p class="menu-label">Administration</p>
<ul class="menu-list">
<li>
<a id="itemManageUploads" disabled>Manage uploads</a>
</li>
<li>
<a id="itemManageUsers" disabled>Manage users</a>
</li>
</ul>
<p class="menu-label">Configuration</p>
<ul class="menu-list">
<li>
<a id="ShareX">ShareX user profile</a>