mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-18 17:21:33 +00:00
refactor: HUGE REFACTOR for hyper-express
This commit is contained in:
parent
b89945d693
commit
38d86779ae
File diff suppressed because it is too large
Load Diff
@ -30,105 +30,108 @@ const self = {
|
||||
// https://github.com/kelektiv/node.bcrypt.js/tree/v5.0.1#a-note-on-rounds
|
||||
const saltRounds = 10
|
||||
|
||||
self.verify = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (!username) throw new ClientError('No username provided.')
|
||||
self.verify = async (req, res) => {
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (!password) throw new ClientError('No password provided.')
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (!username) throw new ClientError('No username provided.')
|
||||
|
||||
const user = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (!password) throw new ClientError('No password provided.')
|
||||
|
||||
if (!user) throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||
const user = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
|
||||
if (user.enabled === false || user.enabled === 0) {
|
||||
throw new ClientError('This account has been disabled.', { statusCode: 403 })
|
||||
}
|
||||
if (!user) throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||
|
||||
const result = await bcrypt.compare(password, user.password)
|
||||
if (result === false) {
|
||||
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||
} else {
|
||||
await res.json({ success: true, token: user.token })
|
||||
}
|
||||
}).catch(next)
|
||||
if (user.enabled === false || user.enabled === 0) {
|
||||
throw new ClientError('This account has been disabled.', { statusCode: 403 })
|
||||
}
|
||||
|
||||
const result = await bcrypt.compare(password, user.password)
|
||||
if (result === false) {
|
||||
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||
} else {
|
||||
return res.json({ success: true, token: user.token })
|
||||
}
|
||||
}
|
||||
|
||||
self.register = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
if (config.enableUserAccounts === false) {
|
||||
throw new ClientError('Registration is currently disabled.', { statusCode: 403 })
|
||||
}
|
||||
self.register = async (req, res) => {
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (username.length < self.user.min || username.length > self.user.max) {
|
||||
throw new ClientError(`Username must have ${self.user.min}-${self.user.max} characters.`)
|
||||
}
|
||||
if (config.enableUserAccounts === false) {
|
||||
throw new ClientError('Registration is currently disabled.', { statusCode: 403 })
|
||||
}
|
||||
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
}
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (username.length < self.user.min || username.length > self.user.max) {
|
||||
throw new ClientError(`Username must have ${self.user.min}-${self.user.max} characters.`)
|
||||
}
|
||||
|
||||
const user = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
}
|
||||
|
||||
if (user) throw new ClientError('Username already exists.')
|
||||
const user = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
if (user) throw new ClientError('Username already exists.')
|
||||
|
||||
const token = await tokens.generateUniqueToken()
|
||||
if (!token) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
await utils.db.table('users')
|
||||
.insert({
|
||||
username,
|
||||
password: hash,
|
||||
token,
|
||||
enabled: 1,
|
||||
permission: perms.permissions.user,
|
||||
registration: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
utils.invalidateStatsCache('users')
|
||||
tokens.onHold.delete(token)
|
||||
const token = await tokens.generateUniqueToken()
|
||||
if (!token) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
|
||||
await res.json({ success: true, token })
|
||||
}).catch(next)
|
||||
await utils.db.table('users')
|
||||
.insert({
|
||||
username,
|
||||
password: hash,
|
||||
token,
|
||||
enabled: 1,
|
||||
permission: perms.permissions.user,
|
||||
registration: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
utils.invalidateStatsCache('users')
|
||||
tokens.onHold.delete(token)
|
||||
|
||||
return res.json({ success: true, token })
|
||||
}
|
||||
|
||||
self.changePassword = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
self.changePassword = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
}
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
const password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('id', user.id)
|
||||
.update('password', hash)
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
await res.json({ success: true })
|
||||
}).catch(next)
|
||||
await utils.db.table('users')
|
||||
.where('id', user.id)
|
||||
.update('password', hash)
|
||||
|
||||
return res.json({ success: true })
|
||||
}
|
||||
|
||||
self.assertPermission = (user, target) => {
|
||||
@ -141,238 +144,253 @@ self.assertPermission = (user, target) => {
|
||||
}
|
||||
}
|
||||
|
||||
self.createUser = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
self.createUser = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) return res.status(403).end()
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (username.length < self.user.min || username.length > self.user.max) {
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) return res.status(403).end()
|
||||
|
||||
const username = typeof req.body.username === 'string'
|
||||
? req.body.username.trim()
|
||||
: ''
|
||||
if (username.length < self.user.min || username.length > self.user.max) {
|
||||
throw new ClientError(`Username must have ${self.user.min}-${self.user.max} characters.`)
|
||||
}
|
||||
|
||||
let password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length) {
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
}
|
||||
} else {
|
||||
password = randomstring.generate(self.pass.rand)
|
||||
}
|
||||
|
||||
let group = req.body.group
|
||||
let permission
|
||||
if (group !== undefined) {
|
||||
permission = perms.permissions[group]
|
||||
if (typeof permission !== 'number' || permission < 0) {
|
||||
group = 'user'
|
||||
permission = perms.permissions.user
|
||||
}
|
||||
}
|
||||
|
||||
const exists = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
|
||||
if (exists) throw new ClientError('Username already exists.')
|
||||
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
const token = await tokens.generateUniqueToken()
|
||||
if (!token) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.insert({
|
||||
username,
|
||||
password: hash,
|
||||
token,
|
||||
enabled: 1,
|
||||
permission,
|
||||
registration: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
utils.invalidateStatsCache('users')
|
||||
tokens.onHold.delete(token)
|
||||
|
||||
return res.json({ success: true, username, password, group })
|
||||
}
|
||||
|
||||
self.editUser = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
// Parse POST body, if required
|
||||
req.body = req.body || await req.json()
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const id = parseInt(req.body.id)
|
||||
if (isNaN(id)) throw new ClientError('No user specified.')
|
||||
|
||||
const target = await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
self.assertPermission(user, target)
|
||||
|
||||
const update = {}
|
||||
|
||||
if (req.body.username !== undefined) {
|
||||
update.username = String(req.body.username).trim()
|
||||
if (update.username.length < self.user.min || update.username.length > self.user.max) {
|
||||
throw new ClientError(`Username must have ${self.user.min}-${self.user.max} characters.`)
|
||||
}
|
||||
}
|
||||
|
||||
let password = typeof req.body.password === 'string'
|
||||
? req.body.password.trim()
|
||||
: ''
|
||||
if (password.length) {
|
||||
if (password.length < self.pass.min || password.length > self.pass.max) {
|
||||
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
|
||||
if (req.body.enabled !== undefined) {
|
||||
update.enabled = Boolean(req.body.enabled)
|
||||
}
|
||||
|
||||
if (req.body.group !== undefined) {
|
||||
update.permission = perms.permissions[req.body.group]
|
||||
if (typeof update.permission !== 'number' || update.permission < 0) {
|
||||
update.permission = target.permission
|
||||
}
|
||||
}
|
||||
|
||||
let password
|
||||
if (req.body.resetPassword) {
|
||||
password = randomstring.generate(self.pass.rand)
|
||||
update.password = await bcrypt.hash(password, saltRounds)
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.update(update)
|
||||
utils.invalidateStatsCache('users')
|
||||
|
||||
const response = { success: true, update }
|
||||
if (password) {
|
||||
response.update.password = password
|
||||
}
|
||||
|
||||
return res.json(response)
|
||||
}
|
||||
|
||||
self.disableUser = async (req, res) => {
|
||||
// Parse POST body and re-map for .editUser()
|
||||
req.body = await req.json()
|
||||
.then(obj => {
|
||||
return {
|
||||
id: obj.id,
|
||||
enabled: false
|
||||
}
|
||||
})
|
||||
return self.editUser(req, res)
|
||||
}
|
||||
|
||||
self.deleteUser = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const id = parseInt(req.body.id)
|
||||
const purge = req.body.purge
|
||||
if (isNaN(id)) throw new ClientError('No user specified.')
|
||||
|
||||
const target = await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
self.assertPermission(user, target)
|
||||
|
||||
const files = await utils.db.table('files')
|
||||
.where('userid', id)
|
||||
.select('id')
|
||||
|
||||
if (files.length) {
|
||||
const fileids = files.map(file => file.id)
|
||||
if (purge) {
|
||||
const failed = await utils.bulkDeleteFromDb('id', fileids, user)
|
||||
utils.invalidateStatsCache('uploads')
|
||||
if (failed.length) {
|
||||
return res.json({ success: false, failed })
|
||||
}
|
||||
} else {
|
||||
password = randomstring.generate(self.pass.rand)
|
||||
// Clear out userid attribute from the files
|
||||
await utils.db.table('files')
|
||||
.whereIn('id', fileids)
|
||||
.update('userid', null)
|
||||
}
|
||||
}
|
||||
|
||||
let group = req.body.group
|
||||
let permission
|
||||
if (group !== undefined) {
|
||||
permission = perms.permissions[group]
|
||||
if (typeof permission !== 'number' || permission < 0) {
|
||||
group = 'user'
|
||||
permission = perms.permissions.user
|
||||
}
|
||||
}
|
||||
const albums = await utils.db.table('albums')
|
||||
.where('userid', id)
|
||||
.where('enabled', 1)
|
||||
.select('id', 'identifier')
|
||||
|
||||
const exists = await utils.db.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
|
||||
if (exists) throw new ClientError('Username already exists.')
|
||||
|
||||
const hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
const token = await tokens.generateUniqueToken()
|
||||
if (!token) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.insert({
|
||||
username,
|
||||
password: hash,
|
||||
token,
|
||||
enabled: 1,
|
||||
permission,
|
||||
registration: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
utils.invalidateStatsCache('users')
|
||||
tokens.onHold.delete(token)
|
||||
|
||||
await res.json({ success: true, username, password, group })
|
||||
}).catch(next)
|
||||
}
|
||||
|
||||
self.editUser = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const id = parseInt(req.body.id)
|
||||
if (isNaN(id)) throw new ClientError('No user specified.')
|
||||
|
||||
const target = await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
self.assertPermission(user, target)
|
||||
|
||||
const update = {}
|
||||
|
||||
if (req.body.username !== undefined) {
|
||||
update.username = String(req.body.username).trim()
|
||||
if (update.username.length < self.user.min || update.username.length > self.user.max) {
|
||||
throw new ClientError(`Username must have ${self.user.min}-${self.user.max} characters.`)
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.enabled !== undefined) {
|
||||
update.enabled = Boolean(req.body.enabled)
|
||||
}
|
||||
|
||||
if (req.body.group !== undefined) {
|
||||
update.permission = perms.permissions[req.body.group]
|
||||
if (typeof update.permission !== 'number' || update.permission < 0) {
|
||||
update.permission = target.permission
|
||||
}
|
||||
}
|
||||
|
||||
let password
|
||||
if (req.body.resetPassword) {
|
||||
password = randomstring.generate(self.pass.rand)
|
||||
update.password = await bcrypt.hash(password, saltRounds)
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.update(update)
|
||||
utils.invalidateStatsCache('users')
|
||||
|
||||
const response = { success: true, update }
|
||||
if (password) response.update.password = password
|
||||
await res.json(response)
|
||||
}).catch(next)
|
||||
}
|
||||
|
||||
self.disableUser = (req, res, next) => {
|
||||
req.body = { id: req.body.id, enabled: false }
|
||||
return self.editUser(req, res, next)
|
||||
}
|
||||
|
||||
self.deleteUser = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const id = parseInt(req.body.id)
|
||||
const purge = req.body.purge
|
||||
if (isNaN(id)) throw new ClientError('No user specified.')
|
||||
|
||||
const target = await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
self.assertPermission(user, target)
|
||||
|
||||
const files = await utils.db.table('files')
|
||||
.where('userid', id)
|
||||
.select('id')
|
||||
|
||||
if (files.length) {
|
||||
const fileids = files.map(file => file.id)
|
||||
if (purge) {
|
||||
const failed = await utils.bulkDeleteFromDb('id', fileids, user)
|
||||
if (failed.length) return res.json({ success: false, failed })
|
||||
utils.invalidateStatsCache('uploads')
|
||||
} else {
|
||||
// Clear out userid attribute from the files
|
||||
await utils.db.table('files')
|
||||
.whereIn('id', fileids)
|
||||
.update('userid', null)
|
||||
}
|
||||
}
|
||||
|
||||
const albums = await utils.db.table('albums')
|
||||
.where('userid', id)
|
||||
.where('enabled', 1)
|
||||
.select('id', 'identifier')
|
||||
|
||||
if (albums.length) {
|
||||
const albumids = albums.map(album => album.id)
|
||||
await utils.db.table('albums')
|
||||
.whereIn('id', albumids)
|
||||
.del()
|
||||
utils.deleteStoredAlbumRenders(albumids)
|
||||
|
||||
// Unlink their archives
|
||||
await Promise.all(albums.map(async album => {
|
||||
try {
|
||||
await paths.unlink(path.join(paths.zips, `${album.identifier}.zip`))
|
||||
} catch (error) {
|
||||
// Re-throw non-ENOENT error
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('id', id)
|
||||
if (albums.length) {
|
||||
const albumids = albums.map(album => album.id)
|
||||
await utils.db.table('albums')
|
||||
.whereIn('id', albumids)
|
||||
.del()
|
||||
utils.invalidateStatsCache('users')
|
||||
utils.deleteStoredAlbumRenders(albumids)
|
||||
|
||||
await res.json({ success: true })
|
||||
}).catch(next)
|
||||
// Unlink their archives
|
||||
await Promise.all(albums.map(async album => {
|
||||
try {
|
||||
await paths.unlink(path.join(paths.zips, `${album.identifier}.zip`))
|
||||
} catch (error) {
|
||||
// Re-throw non-ENOENT error
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('id', id)
|
||||
.del()
|
||||
utils.invalidateStatsCache('users')
|
||||
|
||||
return res.json({ success: true })
|
||||
}
|
||||
|
||||
self.bulkDeleteUsers = (req, res, next) => {
|
||||
self.bulkDeleteUsers = async (req, res) => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
self.listUsers = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
self.listUsers = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const count = await utils.db.table('users')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
if (!count) return res.json({ success: true, users: [], count })
|
||||
const count = await utils.db.table('users')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
if (!count) {
|
||||
return res.json({ success: true, users: [], count })
|
||||
}
|
||||
|
||||
let offset = Number(req.params.page)
|
||||
if (isNaN(offset)) offset = 0
|
||||
else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset)
|
||||
let offset = Number(req.params.page)
|
||||
if (isNaN(offset)) offset = 0
|
||||
else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset)
|
||||
|
||||
const users = await utils.db.table('users')
|
||||
.limit(25)
|
||||
.offset(25 * offset)
|
||||
.select('id', 'username', 'enabled', 'timestamp', 'permission', 'registration')
|
||||
const users = await utils.db.table('users')
|
||||
.limit(25)
|
||||
.offset(25 * offset)
|
||||
.select('id', 'username', 'enabled', 'timestamp', 'permission', 'registration')
|
||||
|
||||
const pointers = {}
|
||||
for (const user of users) {
|
||||
user.groups = perms.mapPermissions(user)
|
||||
delete user.permission
|
||||
user.uploads = 0
|
||||
user.usage = 0
|
||||
pointers[user.id] = user
|
||||
}
|
||||
const pointers = {}
|
||||
for (const user of users) {
|
||||
user.groups = perms.mapPermissions(user)
|
||||
delete user.permission
|
||||
user.uploads = 0
|
||||
user.usage = 0
|
||||
pointers[user.id] = user
|
||||
}
|
||||
|
||||
const uploads = await utils.db.table('files')
|
||||
.whereIn('userid', Object.keys(pointers))
|
||||
.select('userid', 'size')
|
||||
const uploads = await utils.db.table('files')
|
||||
.whereIn('userid', Object.keys(pointers))
|
||||
.select('userid', 'size')
|
||||
|
||||
for (const upload of uploads) {
|
||||
pointers[upload.userid].uploads++
|
||||
pointers[upload.userid].usage += parseInt(upload.size)
|
||||
}
|
||||
for (const upload of uploads) {
|
||||
pointers[upload.userid].uploads++
|
||||
pointers[upload.userid].usage += parseInt(upload.size)
|
||||
}
|
||||
|
||||
await res.json({ success: true, users, count })
|
||||
}).catch(next)
|
||||
return res.json({ success: true, users, count })
|
||||
}
|
||||
|
||||
module.exports = self
|
||||
|
@ -2,8 +2,8 @@ const path = require('path')
|
||||
const paths = require('./pathsController')
|
||||
const ClientError = require('./utils/ClientError')
|
||||
const ServerError = require('./utils/ServerError')
|
||||
const config = require('./../config')
|
||||
const logger = require('./../logger')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
const self = {
|
||||
errorPagesCodes: Object.keys(config.errorPages)
|
||||
@ -11,7 +11,7 @@ const self = {
|
||||
.map(key => Number(key))
|
||||
}
|
||||
|
||||
self.handle = (error, req, res, next) => {
|
||||
self.handlerError = (req, res, error) => {
|
||||
if (!res || res.headersSent) {
|
||||
console.error('Unexpected missing "res" object or headers alredy sent.')
|
||||
return console.trace()
|
||||
@ -58,7 +58,7 @@ self.handle = (error, req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
self.handleMissing = (req, res, next) => {
|
||||
self.handlerNotFound = (req, res) => {
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
return res.status(404).sendFile(path.join(paths.errorRoot, config.errorPages[404]))
|
||||
}
|
||||
|
@ -33,72 +33,69 @@ self.generateUniqueToken = async () => {
|
||||
return null
|
||||
}
|
||||
|
||||
self.verify = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const token = typeof req.body.token === 'string'
|
||||
? req.body.token.trim()
|
||||
: ''
|
||||
self.verify = async (req, res) => {
|
||||
// Parse POST body
|
||||
req.body = await req.json()
|
||||
|
||||
if (!token) throw new ClientError('No token provided.', { statusCode: 403 })
|
||||
const token = typeof req.body.token === 'string'
|
||||
? req.body.token.trim()
|
||||
: ''
|
||||
|
||||
const user = await utils.db.table('users')
|
||||
.where('token', token)
|
||||
.select('username', 'permission')
|
||||
.first()
|
||||
if (!token) throw new ClientError('No token provided.', { statusCode: 403 })
|
||||
|
||||
if (!user) {
|
||||
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||
const user = await utils.db.table('users')
|
||||
.where('token', token)
|
||||
.select('username', 'permission')
|
||||
.first()
|
||||
|
||||
if (!user) {
|
||||
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||
}
|
||||
|
||||
const obj = {
|
||||
success: true,
|
||||
username: user.username,
|
||||
permissions: perms.mapPermissions(user)
|
||||
}
|
||||
|
||||
const group = perms.group(user)
|
||||
if (group) {
|
||||
obj.group = group
|
||||
if (utils.retentions.enabled) {
|
||||
obj.retentionPeriods = utils.retentions.periods[group]
|
||||
obj.defaultRetentionPeriod = utils.retentions.default[group]
|
||||
}
|
||||
}
|
||||
|
||||
const obj = {
|
||||
success: true,
|
||||
username: user.username,
|
||||
permissions: perms.mapPermissions(user)
|
||||
}
|
||||
if (utils.clientVersion) {
|
||||
obj.version = utils.clientVersion
|
||||
}
|
||||
|
||||
const group = perms.group(user)
|
||||
if (group) {
|
||||
obj.group = group
|
||||
if (utils.retentions.enabled) {
|
||||
obj.retentionPeriods = utils.retentions.periods[group]
|
||||
obj.defaultRetentionPeriod = utils.retentions.default[group]
|
||||
}
|
||||
}
|
||||
|
||||
if (utils.clientVersion) {
|
||||
obj.version = utils.clientVersion
|
||||
}
|
||||
|
||||
await res.json(obj)
|
||||
}).catch(next)
|
||||
return res.json(obj)
|
||||
}
|
||||
|
||||
self.list = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req)
|
||||
await res.json({ success: true, token: user.token })
|
||||
}).catch(next)
|
||||
self.list = async (req, res) => {
|
||||
const user = await utils.authorize(req)
|
||||
return res.json({ success: true, token: user.token })
|
||||
}
|
||||
|
||||
self.change = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await utils.authorize(req, 'token')
|
||||
self.change = async (req, res) => {
|
||||
const user = await utils.authorize(req, 'token')
|
||||
|
||||
const newToken = await self.generateUniqueToken()
|
||||
if (!newToken) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
const newToken = await self.generateUniqueToken()
|
||||
if (!newToken) {
|
||||
throw new ServerError('Failed to allocate a unique token. Try again?')
|
||||
}
|
||||
|
||||
await utils.db.table('users')
|
||||
.where('token', user.token)
|
||||
.update({
|
||||
token: newToken,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
self.onHold.delete(newToken)
|
||||
await utils.db.table('users')
|
||||
.where('token', user.token)
|
||||
.update({
|
||||
token: newToken,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
self.onHold.delete(newToken)
|
||||
|
||||
await res.json({ success: true, token: newToken })
|
||||
}).catch(next)
|
||||
return res.json({ success: true, token: newToken })
|
||||
}
|
||||
|
||||
module.exports = self
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -784,320 +784,324 @@ self.invalidateStatsCache = type => {
|
||||
statsData[type].cache = null
|
||||
}
|
||||
|
||||
self.stats = (req, res, next) => {
|
||||
Promise.resolve().then(async () => {
|
||||
const user = await self.authorize(req)
|
||||
const generateStats = async (req, res) => {
|
||||
const user = await self.authorize(req)
|
||||
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
const isadmin = perms.is(user, 'admin')
|
||||
if (!isadmin) throw new ClientError('', { statusCode: 403 })
|
||||
|
||||
const hrstart = process.hrtime()
|
||||
const stats = {}
|
||||
Object.keys(statsData).forEach(key => {
|
||||
// Pre-assign object keys to fix their display order
|
||||
stats[statsData[key].title] = {}
|
||||
})
|
||||
const hrstart = process.hrtime()
|
||||
const stats = {}
|
||||
Object.keys(statsData).forEach(key => {
|
||||
// Pre-assign object keys to fix their display order
|
||||
stats[statsData[key].title] = {}
|
||||
})
|
||||
|
||||
const os = await si.osInfo()
|
||||
const os = await si.osInfo()
|
||||
|
||||
const getSystemInfo = async () => {
|
||||
const data = statsData.system
|
||||
const getSystemInfo = async () => {
|
||||
const data = statsData.system
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (((Date.now() - data.generatedAt) <= 500) || data.generating) {
|
||||
// Use cache for 500 ms (0.5 seconds)
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (((Date.now() - data.generatedAt) <= 500) || data.generating) {
|
||||
// Use cache for 500 ms (0.5 seconds)
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
const currentLoad = await si.currentLoad()
|
||||
const mem = await si.mem()
|
||||
const time = si.time()
|
||||
const nodeUptime = process.uptime()
|
||||
const currentLoad = await si.currentLoad()
|
||||
const mem = await si.mem()
|
||||
const time = si.time()
|
||||
const nodeUptime = process.uptime()
|
||||
|
||||
if (self.scan.instance) {
|
||||
try {
|
||||
self.scan.version = await self.scan.instance.getVersion().then(s => s.trim())
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
self.scan.version = 'Errored when querying version.'
|
||||
}
|
||||
if (self.scan.instance) {
|
||||
try {
|
||||
self.scan.version = await self.scan.instance.getVersion().then(s => s.trim())
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
self.scan.version = 'Errored when querying version.'
|
||||
}
|
||||
|
||||
stats[data.title] = {
|
||||
Platform: `${os.platform} ${os.arch}`,
|
||||
Distro: `${os.distro} ${os.release}`,
|
||||
Kernel: os.kernel,
|
||||
Scanner: self.scan.version || 'N/A',
|
||||
'CPU Load': `${currentLoad.currentLoad.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: 'byteUsage'
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: 'byte'
|
||||
},
|
||||
'System Uptime': {
|
||||
value: Math.floor(time.uptime),
|
||||
type: 'uptime'
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: 'uptime'
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
const getFileSystems = async () => {
|
||||
const data = statsData.fileSystems
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (((Date.now() - data.generatedAt) <= 60000) || data.generating) {
|
||||
// Use cache for 60000 ms (60 seconds)
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
stats[data.title] = {}
|
||||
|
||||
const fsSize = await si.fsSize()
|
||||
for (const fs of fsSize) {
|
||||
const obj = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: 'byteUsage'
|
||||
}
|
||||
// "available" is a new attribute in systeminformation v5, only tested on Linux,
|
||||
// so add an if-check just in case its availability is limited in other platforms
|
||||
if (typeof fs.available === 'number') {
|
||||
obj.value.available = fs.available
|
||||
}
|
||||
stats[data.title][`${fs.fs} (${fs.type}) on ${fs.mount}`] = obj
|
||||
stats[data.title] = {
|
||||
Platform: `${os.platform} ${os.arch}`,
|
||||
Distro: `${os.distro} ${os.release}`,
|
||||
Kernel: os.kernel,
|
||||
Scanner: self.scan.version || 'N/A',
|
||||
'CPU Load': `${currentLoad.currentLoad.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: 'byteUsage'
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: 'byte'
|
||||
},
|
||||
'System Uptime': {
|
||||
value: Math.floor(time.uptime),
|
||||
type: 'uptime'
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: 'uptime'
|
||||
}
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
const getUploadsStats = async () => {
|
||||
const data = statsData.uploads
|
||||
const getFileSystems = async () => {
|
||||
const data = statsData.fileSystems
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (((Date.now() - data.generatedAt) <= 60000) || data.generating) {
|
||||
// Use cache for 60000 ms (60 seconds)
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Images: 0,
|
||||
Videos: 0,
|
||||
Audios: 0,
|
||||
Others: 0,
|
||||
Temporary: 0,
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: 'byte'
|
||||
}
|
||||
stats[data.title] = {}
|
||||
|
||||
const fsSize = await si.fsSize()
|
||||
for (const fs of fsSize) {
|
||||
const obj = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: 'byteUsage'
|
||||
}
|
||||
|
||||
const getTotalCountAndSize = async () => {
|
||||
const uploads = await self.db.table('files')
|
||||
.select('size')
|
||||
stats[data.title].Total = uploads.length
|
||||
stats[data.title]['Size in DB'].value = uploads.reduce((acc, upload) => acc + parseInt(upload.size), 0)
|
||||
// "available" is a new attribute in systeminformation v5, only tested on Linux,
|
||||
// so add an if-check just in case its availability is limited in other platforms
|
||||
if (typeof fs.available === 'number') {
|
||||
obj.value.available = fs.available
|
||||
}
|
||||
stats[data.title][`${fs.fs} (${fs.type}) on ${fs.mount}`] = obj
|
||||
}
|
||||
|
||||
const getImagesCount = async () => {
|
||||
stats[data.title].Images = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.imageExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
const getUploadsStats = async () => {
|
||||
const data = statsData.uploads
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Images: 0,
|
||||
Videos: 0,
|
||||
Audios: 0,
|
||||
Others: 0,
|
||||
Temporary: 0,
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: 'byte'
|
||||
}
|
||||
}
|
||||
|
||||
const getVideosCount = async () => {
|
||||
stats[data.title].Videos = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.videoExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
const getTotalCountAndSize = async () => {
|
||||
const uploads = await self.db.table('files')
|
||||
.select('size')
|
||||
stats[data.title].Total = uploads.length
|
||||
stats[data.title]['Size in DB'].value = uploads.reduce((acc, upload) => acc + parseInt(upload.size), 0)
|
||||
}
|
||||
|
||||
const getAudiosCount = async () => {
|
||||
stats[data.title].Audios = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.audioExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
const getImagesCount = async () => {
|
||||
stats[data.title].Images = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.imageExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
|
||||
const getOthersCount = async () => {
|
||||
stats[data.title].Temporary = await self.db.table('files')
|
||||
.whereNotNull('expirydate')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
const getVideosCount = async () => {
|
||||
stats[data.title].Videos = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.videoExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
getTotalCountAndSize(),
|
||||
getImagesCount(),
|
||||
getVideosCount(),
|
||||
getAudiosCount(),
|
||||
getOthersCount()
|
||||
])
|
||||
const getAudiosCount = async () => {
|
||||
stats[data.title].Audios = await self.db.table('files')
|
||||
.where(function () {
|
||||
for (const ext of self.audioExts) {
|
||||
this.orWhere('name', 'like', `%${ext}`)
|
||||
}
|
||||
})
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
|
||||
stats[data.title].Others = stats[data.title].Total -
|
||||
const getOthersCount = async () => {
|
||||
stats[data.title].Temporary = await self.db.table('files')
|
||||
.whereNotNull('expirydate')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
getTotalCountAndSize(),
|
||||
getImagesCount(),
|
||||
getVideosCount(),
|
||||
getAudiosCount(),
|
||||
getOthersCount()
|
||||
])
|
||||
|
||||
stats[data.title].Others = stats[data.title].Total -
|
||||
stats[data.title].Images -
|
||||
stats[data.title].Videos -
|
||||
stats[data.title].Audios
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
const getUsersStats = async () => {
|
||||
const data = statsData.users
|
||||
const getUsersStats = async () => {
|
||||
const data = statsData.users
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Disabled: 0
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Disabled: 0
|
||||
}
|
||||
|
||||
const permissionKeys = Object.keys(perms.permissions).reverse()
|
||||
permissionKeys.forEach(p => {
|
||||
stats[data.title][p] = 0
|
||||
})
|
||||
|
||||
const users = await self.db.table('users')
|
||||
stats[data.title].Total = users.length
|
||||
for (const user of users) {
|
||||
if (user.enabled === false || user.enabled === 0) {
|
||||
stats[data.title].Disabled++
|
||||
}
|
||||
|
||||
const permissionKeys = Object.keys(perms.permissions).reverse()
|
||||
permissionKeys.forEach(p => {
|
||||
stats[data.title][p] = 0
|
||||
})
|
||||
|
||||
const users = await self.db.table('users')
|
||||
stats[data.title].Total = users.length
|
||||
for (const user of users) {
|
||||
if (user.enabled === false || user.enabled === 0) {
|
||||
stats[data.title].Disabled++
|
||||
}
|
||||
|
||||
user.permission = user.permission || 0
|
||||
for (const p of permissionKeys) {
|
||||
if (user.permission === perms.permissions[p]) {
|
||||
stats[data.title][p]++
|
||||
break
|
||||
}
|
||||
user.permission = user.permission || 0
|
||||
for (const p of permissionKeys) {
|
||||
if (user.permission === perms.permissions[p]) {
|
||||
stats[data.title][p]++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
const getAlbumsStats = async () => {
|
||||
const data = statsData.albums
|
||||
const getAlbumsStats = async () => {
|
||||
const data = statsData.albums
|
||||
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
if (!data.cache && data.generating) {
|
||||
stats[data.title] = false
|
||||
} else if (data.cache) {
|
||||
// Cache will be invalidated with self.invalidateStatsCache() after any related operations
|
||||
stats[data.title] = data.cache
|
||||
} else {
|
||||
data.generating = true
|
||||
data.generatedAt = Date.now()
|
||||
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Disabled: 0,
|
||||
Public: 0,
|
||||
Downloadable: 0,
|
||||
'ZIP Generated': 0
|
||||
}
|
||||
|
||||
const albums = await self.db.table('albums')
|
||||
stats[data.title].Total = albums.length
|
||||
|
||||
const activeAlbums = []
|
||||
for (const album of albums) {
|
||||
if (!album.enabled) {
|
||||
stats[data.title].Disabled++
|
||||
continue
|
||||
}
|
||||
activeAlbums.push(album.id)
|
||||
if (album.download) stats[data.title].Downloadable++
|
||||
if (album.public) stats[data.title].Public++
|
||||
}
|
||||
|
||||
await paths.readdir(paths.zips).then(files => {
|
||||
stats[data.title]['ZIP Generated'] = files.length
|
||||
}).catch(() => {})
|
||||
|
||||
stats[data.title]['Files in albums'] = await self.db.table('files')
|
||||
.whereIn('albumid', activeAlbums)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
stats[data.title] = {
|
||||
Total: 0,
|
||||
Disabled: 0,
|
||||
Public: 0,
|
||||
Downloadable: 0,
|
||||
'ZIP Generated': 0
|
||||
}
|
||||
|
||||
const albums = await self.db.table('albums')
|
||||
stats[data.title].Total = albums.length
|
||||
|
||||
const activeAlbums = []
|
||||
for (const album of albums) {
|
||||
if (!album.enabled) {
|
||||
stats[data.title].Disabled++
|
||||
continue
|
||||
}
|
||||
activeAlbums.push(album.id)
|
||||
if (album.download) stats[data.title].Downloadable++
|
||||
if (album.public) stats[data.title].Public++
|
||||
}
|
||||
|
||||
await paths.readdir(paths.zips).then(files => {
|
||||
stats[data.title]['ZIP Generated'] = files.length
|
||||
}).catch(() => {})
|
||||
|
||||
stats[data.title]['Files in albums'] = await self.db.table('files')
|
||||
.whereIn('albumid', activeAlbums)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count)
|
||||
|
||||
// Update cache
|
||||
data.cache = stats[data.title]
|
||||
data.generating = false
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
getSystemInfo(),
|
||||
getFileSystems(),
|
||||
getUploadsStats(),
|
||||
getUsersStats(),
|
||||
getAlbumsStats()
|
||||
])
|
||||
await Promise.all([
|
||||
getSystemInfo(),
|
||||
getFileSystems(),
|
||||
getUploadsStats(),
|
||||
getUsersStats(),
|
||||
getAlbumsStats()
|
||||
])
|
||||
|
||||
return res.json({ success: true, stats, hrtime: process.hrtime(hrstart) })
|
||||
}).catch(error => {
|
||||
// Reset generating state when encountering any errors
|
||||
Object.keys(statsData).forEach(key => {
|
||||
statsData[key].generating = false
|
||||
return res.json({ success: true, stats, hrtime: process.hrtime(hrstart) })
|
||||
}
|
||||
|
||||
self.stats = async (req, res) => {
|
||||
return generateStats(req, res)
|
||||
.catch(error => {
|
||||
logger.debug('caught generateStats() errors')
|
||||
// Reset generating state when encountering any errors
|
||||
Object.keys(statsData).forEach(key => {
|
||||
statsData[key].generating = false
|
||||
})
|
||||
throw error
|
||||
})
|
||||
return next(error)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = self
|
||||
|
115
lolisafe.js
115
lolisafe.js
@ -10,14 +10,13 @@ process.on('unhandledRejection', error => {
|
||||
})
|
||||
|
||||
// Require libraries
|
||||
const bodyParser = require('body-parser')
|
||||
const contentDisposition = require('content-disposition')
|
||||
const express = require('express')
|
||||
const helmet = require('helmet')
|
||||
const HyperExpress = require('hyper-express')
|
||||
const LiveDirectory = require('live-directory')
|
||||
const NodeClam = require('clamscan')
|
||||
const nunjucks = require('nunjucks')
|
||||
const path = require('path')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
// const rateLimit = require('express-rate-limit') // FIXME: Find alternative
|
||||
const { accessSync, constants } = require('fs')
|
||||
|
||||
// Check required config files
|
||||
@ -38,9 +37,10 @@ const versions = require('./src/versions')
|
||||
|
||||
// lolisafe
|
||||
logger.log('Starting lolisafe\u2026')
|
||||
const safe = express()
|
||||
const safe = new HyperExpress.Server({
|
||||
trust_proxy: Boolean(config.trustProxy)
|
||||
})
|
||||
|
||||
const errors = require('./controllers/errorsController')
|
||||
const paths = require('./controllers/pathsController')
|
||||
paths.initSync()
|
||||
const utils = require('./controllers/utilsController')
|
||||
@ -91,21 +91,44 @@ if (config.accessControlAllowOrigin) {
|
||||
})
|
||||
}
|
||||
|
||||
if (config.trustProxy) {
|
||||
safe.set('trust proxy', 1)
|
||||
const initLiveDirectory = (options = {}) => {
|
||||
if (!options.ignore) {
|
||||
options.ignore = path => {
|
||||
// ignore dot files
|
||||
return path.startsWith('.')
|
||||
}
|
||||
}
|
||||
return new LiveDirectory(options)
|
||||
}
|
||||
|
||||
// https://mozilla.github.io/nunjucks/api.html#configure
|
||||
nunjucks.configure('views', {
|
||||
const nunjucksEnv = nunjucks.configure('views', {
|
||||
autoescape: true,
|
||||
express: safe,
|
||||
watch: isDevMode
|
||||
// noCache: isDevMode
|
||||
})
|
||||
safe.set('view engine', 'njk')
|
||||
safe.enable('view cache')
|
||||
|
||||
const renderNunjucks = (res, path, params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nunjucksEnv.render(`${path}.njk`, params, (err, html) => {
|
||||
if (err) return reject(err)
|
||||
resolve(html)
|
||||
})
|
||||
}).then(html => {
|
||||
return res.type('html').send(html)
|
||||
})
|
||||
}
|
||||
|
||||
// Bind a global middleware which will attach to our helper method into all incoming requests
|
||||
safe.use((req, res, next) => {
|
||||
// Inject the render method onto the response object on each requet
|
||||
res.render = (path, params) => renderNunjucks(res, path, params)
|
||||
next()
|
||||
})
|
||||
|
||||
// Configure rate limits (disabled during development)
|
||||
// FIXME: express-rate-limit does not work with hyper-express, find alternative
|
||||
/*
|
||||
if (!isDevMode && Array.isArray(config.rateLimits) && config.rateLimits.length) {
|
||||
for (const _rateLimit of config.rateLimits) {
|
||||
const limiter = rateLimit(_rateLimit.config)
|
||||
@ -114,9 +137,7 @@ if (!isDevMode && Array.isArray(config.rateLimits) && config.rateLimits.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
safe.use(bodyParser.urlencoded({ extended: true }))
|
||||
safe.use(bodyParser.json())
|
||||
*/
|
||||
|
||||
const cdnPages = [...config.pages]
|
||||
let setHeaders
|
||||
@ -179,15 +200,17 @@ const initServeStaticUploads = (opts = {}) => {
|
||||
// which it will await before creating 'send' stream to client.
|
||||
// This is necessary due to database queries being async tasks,
|
||||
// and express/serve-static not having the functionality by default.
|
||||
safe.use('/', require('@bobbywibowo/serve-static')(paths.uploads, opts))
|
||||
logger.debug('Inititated SimpleDataStore for Content-Disposition: ' +
|
||||
`{ limit: ${utils.contentDispositionStore.limit}, strategy: "${utils.contentDispositionStore.strategy}" }`)
|
||||
// safe.use('/', require('@bobbywibowo/serve-static')(paths.uploads, opts))
|
||||
// logger.debug('Inititated SimpleDataStore for Content-Disposition: ' +
|
||||
// `{ limit: ${utils.contentDispositionStore.limit}, strategy: "${utils.contentDispositionStore.strategy}" }`)
|
||||
logger.error('initServeStaticUploads() was called, but still WIP')
|
||||
} else {
|
||||
safe.use('/', express.static(paths.uploads, opts))
|
||||
// safe.use('/', express.static(paths.uploads, opts))
|
||||
logger.error('initServeStaticUploads() was called, but still WIP')
|
||||
}
|
||||
}
|
||||
|
||||
// Cache control (safe.fiery.me)
|
||||
// Cache control
|
||||
if (config.cacheControl) {
|
||||
const cacheControls = {
|
||||
// max-age: 6 months
|
||||
@ -212,9 +235,8 @@ if (config.cacheControl) {
|
||||
// If using CDN, cache public pages in CDN
|
||||
cdnPages.push('api/check')
|
||||
for (const page of cdnPages) {
|
||||
safe.get(`/${page === 'home' ? '' : page}`, (req, res, next) => {
|
||||
safe.get(`/${page === 'home' ? '' : page}`, async (req, res) => {
|
||||
res.set('Cache-Control', cacheControls.cdn)
|
||||
next()
|
||||
})
|
||||
}
|
||||
break
|
||||
@ -264,9 +286,25 @@ if (config.cacheControl) {
|
||||
}
|
||||
|
||||
// Static assets
|
||||
safe.use('/', express.static(paths.public, { setHeaders }))
|
||||
safe.use('/', express.static(paths.dist, { setHeaders }))
|
||||
const liveDirectoryPublic = initLiveDirectory({ path: paths.public })
|
||||
const liveDirectoryDist = initLiveDirectory({ path: paths.dist })
|
||||
safe.use('/', (req, res, next) => {
|
||||
// Only process GET and HEAD requests
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
return next()
|
||||
}
|
||||
// Try to find asset from public directory, then dist directory
|
||||
const file = liveDirectoryPublic.get(req.path) || liveDirectoryDist.get(req.path)
|
||||
if (file === undefined) {
|
||||
return next()
|
||||
}
|
||||
if (typeof setHeaders === 'function') {
|
||||
setHeaders(res)
|
||||
}
|
||||
return res.type(file.extension).send(file.buffer)
|
||||
})
|
||||
|
||||
// Routes
|
||||
safe.use('/', album)
|
||||
safe.use('/', file)
|
||||
safe.use('/', nojs)
|
||||
@ -276,7 +314,7 @@ safe.use('/api', api)
|
||||
;(async () => {
|
||||
try {
|
||||
// Init database
|
||||
await require('./controllers/utils/initDatabase.js')(utils.db)
|
||||
await require('./controllers/utils/initDatabase')(utils.db)
|
||||
|
||||
// Purge any leftover in chunks directory, do not wait
|
||||
paths.purgeChunks()
|
||||
@ -297,6 +335,11 @@ safe.use('/api', api)
|
||||
}
|
||||
}
|
||||
|
||||
const liveDirectoryCustomPages = initLiveDirectory({
|
||||
path: paths.customPages,
|
||||
keep: ['.html']
|
||||
})
|
||||
|
||||
// Cookie Policy
|
||||
if (config.cookiePolicy) {
|
||||
config.pages.push('cookiepolicy')
|
||||
@ -304,23 +347,27 @@ safe.use('/api', api)
|
||||
|
||||
// Check for custom pages, otherwise fallback to Nunjucks templates
|
||||
for (const page of config.pages) {
|
||||
const customPage = path.join(paths.customPages, `${page}.html`)
|
||||
if (!await paths.access(customPage).catch(() => true)) {
|
||||
safe.get(`/${page === 'home' ? '' : page}`, (req, res, next) => res.sendFile(customPage))
|
||||
// FIXME: Have this update on-the-fly or don't use LiveDirectory
|
||||
const customPage = liveDirectoryCustomPages.get(`${page}.html`)
|
||||
if (customPage) {
|
||||
safe.get(`/${page === 'home' ? '' : page}`, (req, res) => {
|
||||
res.type('html').send(customPage.buffer)
|
||||
})
|
||||
} else if (page === 'home') {
|
||||
safe.get('/', (req, res, next) => res.render(page, {
|
||||
safe.get('/', (req, res) => res.render(page, {
|
||||
config, utils, versions: utils.versionStrings
|
||||
}))
|
||||
} else {
|
||||
safe.get(`/${page}`, (req, res, next) => res.render(page, {
|
||||
safe.get(`/${page}`, (req, res) => res.render(page, {
|
||||
config, utils, versions: utils.versionStrings
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Express error handlers
|
||||
safe.use(errors.handleMissing)
|
||||
safe.use(errors.handle)
|
||||
// Web server error handlers (must always be set after all routes/routers)
|
||||
const errorsController = require('./controllers/errorsController')
|
||||
safe.set_not_found_handler(errorsController.handlerNotFound)
|
||||
safe.set_error_handler(errorsController.handlerError)
|
||||
|
||||
// Git hash
|
||||
if (config.showGitHash) {
|
||||
@ -355,7 +402,7 @@ safe.use('/api', api)
|
||||
}
|
||||
|
||||
// Binds Express to port
|
||||
await new Promise(resolve => safe.listen(utils.conf.port, () => resolve()))
|
||||
await safe.listen(utils.conf.port)
|
||||
logger.log(`lolisafe started on port ${utils.conf.port}`)
|
||||
|
||||
// Cache control (safe.fiery.me)
|
||||
|
@ -1,13 +1,14 @@
|
||||
const routes = require('express').Router()
|
||||
const { Router } = require('hyper-express')
|
||||
const routes = new Router()
|
||||
const path = require('path')
|
||||
const paths = require('./../controllers/pathsController')
|
||||
const errors = require('../controllers/errorsController')
|
||||
const utils = require('./../controllers/utilsController')
|
||||
const config = require('./../config')
|
||||
|
||||
routes.get('/a/:identifier', async (req, res, next) => {
|
||||
routes.get('/a/:identifier', async (req, res) => {
|
||||
const identifier = req.params.identifier
|
||||
if (identifier === undefined) {
|
||||
res.status(404).sendFile(path.join(paths.errorRoot, config.errorPages[404]))
|
||||
return errors.handlerNotFound(req, res)
|
||||
}
|
||||
|
||||
const album = await utils.db.table('albums')
|
||||
@ -19,7 +20,7 @@ routes.get('/a/:identifier', async (req, res, next) => {
|
||||
.first()
|
||||
|
||||
if (!album || album.public === 0) {
|
||||
return res.status(404).sendFile(path.join(paths.errorRoot, config.errorPages[404]))
|
||||
return errors.handlerNotFound(req, res)
|
||||
}
|
||||
|
||||
const nojs = req.query.nojs !== undefined
|
||||
|
@ -1,4 +1,5 @@
|
||||
const routes = require('express').Router()
|
||||
const { Router } = require('hyper-express')
|
||||
const routes = new Router()
|
||||
const albumsController = require('./../controllers/albumsController')
|
||||
const authController = require('./../controllers/authController')
|
||||
const tokenController = require('./../controllers/tokenController')
|
||||
@ -6,7 +7,7 @@ const uploadController = require('./../controllers/uploadController')
|
||||
const utilsController = require('./../controllers/utilsController')
|
||||
const config = require('./../config')
|
||||
|
||||
routes.get('/check', (req, res, next) => {
|
||||
routes.get('/check', async (req, res) => {
|
||||
const obj = {
|
||||
private: config.private,
|
||||
enableUserAccounts: config.enableUserAccounts,
|
||||
@ -22,44 +23,51 @@ routes.get('/check', (req, res, next) => {
|
||||
if (utilsController.clientVersion) {
|
||||
obj.version = utilsController.clientVersion
|
||||
}
|
||||
|
||||
return res.json(obj)
|
||||
})
|
||||
|
||||
routes.post('/login', (req, res, next) => authController.verify(req, res, next))
|
||||
routes.post('/register', (req, res, next) => authController.register(req, res, next))
|
||||
routes.post('/password/change', (req, res, next) => authController.changePassword(req, res, next))
|
||||
routes.get('/uploads', (req, res, next) => uploadController.list(req, res, next))
|
||||
routes.get('/uploads/:page', (req, res, next) => uploadController.list(req, res, next))
|
||||
routes.post('/upload', (req, res, next) => uploadController.upload(req, res, next))
|
||||
routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next))
|
||||
routes.post('/upload/bulkdelete', (req, res, next) => uploadController.bulkDelete(req, res, next))
|
||||
routes.post('/upload/finishchunks', (req, res, next) => uploadController.finishChunks(req, res, next))
|
||||
routes.get('/upload/get/:identifier', (req, res, next) => uploadController.get(req, res, next))
|
||||
routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next))
|
||||
routes.get('/album/get/:identifier', (req, res, next) => albumsController.get(req, res, next))
|
||||
routes.get('/album/zip/:identifier', (req, res, next) => albumsController.generateZip(req, res, next))
|
||||
routes.get('/album/:id', (req, res, next) => albumsController.listFiles(req, res, next))
|
||||
routes.get('/album/:id/:page', (req, res, next) => albumsController.listFiles(req, res, next))
|
||||
routes.get('/albums', (req, res, next) => albumsController.list(req, res, next))
|
||||
routes.get('/albums/:page', (req, res, next) => albumsController.list(req, res, next))
|
||||
routes.post('/albums', (req, res, next) => albumsController.create(req, res, next))
|
||||
routes.post('/albums/addfiles', (req, res, next) => albumsController.addFiles(req, res, next))
|
||||
routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next))
|
||||
routes.post('/albums/disable', (req, res, next) => albumsController.disable(req, res, next))
|
||||
routes.post('/albums/edit', (req, res, next) => albumsController.edit(req, res, next))
|
||||
routes.post('/albums/rename', (req, res, next) => albumsController.rename(req, res, next))
|
||||
routes.get('/albums/test', (req, res, next) => albumsController.test(req, res, next))
|
||||
routes.get('/tokens', (req, res, next) => tokenController.list(req, res, next))
|
||||
routes.post('/tokens/verify', (req, res, next) => tokenController.verify(req, res, next))
|
||||
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/create', (req, res, next) => authController.createUser(req, res, next))
|
||||
routes.post('/users/edit', (req, res, next) => authController.editUser(req, res, next))
|
||||
routes.post('/users/disable', (req, res, next) => authController.disableUser(req, res, next))
|
||||
routes.post('/users/delete', (req, res, next) => authController.deleteUser(req, res, next))
|
||||
routes.get('/stats', (req, res, next) => utilsController.stats(req, res, next))
|
||||
routes.post('/login', authController.verify)
|
||||
routes.post('/register', authController.register)
|
||||
routes.post('/password/change', authController.changePassword)
|
||||
routes.get('/uploads', uploadController.list)
|
||||
routes.get('/uploads/:page', uploadController.list)
|
||||
routes.post('/upload', uploadController.upload, {
|
||||
// HyperExpress defaults to 250kb
|
||||
// https://github.com/kartikk221/hyper-express/blob/6.2.4/docs/Server.md#server-constructor-options
|
||||
max_body_length: parseInt(config.uploads.maxSize) * 1e6
|
||||
})
|
||||
routes.post('/uploadurls', uploadController.uploadUrls)
|
||||
routes.post('/upload/delete', uploadController.delete)
|
||||
routes.post('/upload/bulkdelete', uploadController.bulkDelete)
|
||||
routes.post('/upload/finishchunks', uploadController.finishChunks)
|
||||
routes.get('/upload/get/:identifier', uploadController.get)
|
||||
routes.post('/upload/:albumid', uploadController.upload)
|
||||
routes.post('/uploadurls/:albumid', uploadController.uploadUrls)
|
||||
routes.get('/album/get/:identifier', albumsController.get)
|
||||
routes.get('/album/zip/:identifier', albumsController.generateZip)
|
||||
routes.get('/album/:id', albumsController.listFiles)
|
||||
routes.get('/album/:id/:page', albumsController.listFiles)
|
||||
routes.get('/albums', albumsController.list)
|
||||
routes.get('/albums/:page', albumsController.list)
|
||||
routes.post('/albums', albumsController.create)
|
||||
routes.post('/albums/addfiles', albumsController.addFiles)
|
||||
routes.post('/albums/delete', albumsController.delete)
|
||||
routes.post('/albums/disable', albumsController.disable)
|
||||
routes.post('/albums/edit', albumsController.edit)
|
||||
routes.post('/albums/rename', albumsController.rename)
|
||||
routes.get('/albums/test', albumsController.test)
|
||||
routes.get('/tokens', tokenController.list)
|
||||
routes.post('/tokens/verify', tokenController.verify)
|
||||
routes.post('/tokens/change', tokenController.change)
|
||||
routes.get('/filelength/config', authController.getFileLengthConfig)
|
||||
routes.post('/filelength/change', authController.changeFileLength)
|
||||
routes.get('/users', authController.listUsers)
|
||||
routes.get('/users/:page', authController.listUsers)
|
||||
routes.post('/users/create', authController.createUser)
|
||||
routes.post('/users/edit', authController.editUser)
|
||||
routes.post('/users/disable', authController.disableUser)
|
||||
routes.post('/users/delete', authController.deleteUser)
|
||||
routes.get('/stats', utilsController.stats)
|
||||
|
||||
module.exports = routes
|
||||
|
@ -1,10 +1,9 @@
|
||||
const routes = require('express').Router()
|
||||
const { Router } = require('hyper-express')
|
||||
const routes = new Router()
|
||||
const utils = require('../controllers/utilsController')
|
||||
const config = require('../config')
|
||||
|
||||
routes.get([
|
||||
'/file/:identifier'
|
||||
], async (req, res, next) => {
|
||||
routes.get('/file/:identifier', async (req, res) => {
|
||||
// Uploads identifiers parsing, etc., are strictly handled by client-side JS at src/js/file.js
|
||||
return res.render('file', {
|
||||
config, utils, versions: utils.versionStrings
|
||||
|
@ -1,9 +1,10 @@
|
||||
const routes = require('express').Router()
|
||||
const { Router } = require('hyper-express')
|
||||
const routes = new Router()
|
||||
const uploadController = require('./../controllers/uploadController')
|
||||
const utils = require('./../controllers/utilsController')
|
||||
const config = require('./../config')
|
||||
|
||||
routes.get('/nojs', async (req, res, next) => {
|
||||
routes.get('/nojs', async (req, res) => {
|
||||
return res.render('nojs', {
|
||||
config,
|
||||
utils,
|
||||
@ -11,7 +12,7 @@ routes.get('/nojs', async (req, res, next) => {
|
||||
})
|
||||
})
|
||||
|
||||
routes.post('/nojs', (req, res, next) => {
|
||||
routes.post('/nojs', async (req, res) => {
|
||||
res._json = res.json
|
||||
res.json = (...args) => {
|
||||
const result = args[0]
|
||||
@ -23,7 +24,11 @@ routes.post('/nojs', (req, res, next) => {
|
||||
files: result.files || [{}]
|
||||
})
|
||||
}
|
||||
return uploadController.upload(req, res, next)
|
||||
return uploadController.upload(req, res)
|
||||
}, {
|
||||
// HyperExpress defaults to 250kb
|
||||
// https://github.com/kartikk221/hyper-express/blob/6.2.4/docs/Server.md#server-constructor-options
|
||||
max_body_length: parseInt(config.uploads.maxSize) * 1e6
|
||||
})
|
||||
|
||||
module.exports = routes
|
||||
|
@ -1,15 +1,16 @@
|
||||
const routes = require('express').Router()
|
||||
const { Router } = require('hyper-express')
|
||||
const routes = new Router()
|
||||
const utils = require('./../controllers/utilsController')
|
||||
const config = require('./../config')
|
||||
|
||||
routes.get([
|
||||
'/player/:identifier',
|
||||
'/v/:identifier'
|
||||
], async (req, res, next) => {
|
||||
const playerHandler = async (req, res) => {
|
||||
// Uploads identifiers parsing, etc., are strictly handled by client-side JS at src/js/player.js
|
||||
return res.render('player', {
|
||||
config, utils, versions: utils.versionStrings
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
routes.get('/player/:identifier', playerHandler)
|
||||
routes.get('/v/:identifier', playerHandler)
|
||||
|
||||
module.exports = routes
|
||||
|
Loading…
Reference in New Issue
Block a user