Various updates

* Switched ESLint + Aqua to Standard. I'm a big fan of Standard. Updated yarn.lock file too.

* Lots of refactors to follow the rules of Standard.

* Fixed issue with uploading as a not logged in user.
This commit is contained in:
Bobby Wibowo 2018-01-24 03:06:30 +07:00
parent da40ea71b1
commit bcdfcd7064
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
20 changed files with 2153 additions and 1759 deletions

View File

@ -1,84 +1,84 @@
module.exports = { module.exports = {
/* /*
If set to true the user will need to specify the auto-generated token If set to true the user will need to specify the auto-generated token
on each API call, meaning random strangers wont be able to use the service on each API call, meaning random strangers wont be able to use the service
unless they have the token loli-safe provides you with. unless they have the token loli-safe provides you with.
If it's set to false, then upload will be public for anyone to use. If it's set to false, then upload will be public for anyone to use.
*/ */
private: true, private: true,
// If true, users will be able to create accounts and access their uploaded files // If true, users will be able to create accounts and access their uploaded files
enableUserAccounts: true, enableUserAccounts: true,
/* /*
Here you can decide if you want lolisafe to serve the files or if you prefer doing so via nginx. Here you can decide if you want lolisafe to serve the files or if you prefer doing so via nginx.
The main difference between the two is the ease of use and the chance of analytics in the future. The main difference between the two is the ease of use and the chance of analytics in the future.
If you set it to `true`, the uploaded files will be located after the host like: If you set it to `true`, the uploaded files will be located after the host like:
https://lolisafe.moe/yourFile.jpg https://lolisafe.moe/yourFile.jpg
If you set it to `false`, you need to set nginx to directly serve whatever folder it is you are serving your If you set it to `false`, you need to set nginx to directly serve whatever folder it is you are serving your
downloads in. This also gives you the ability to serve them, for example, like this: downloads in. This also gives you the ability to serve them, for example, like this:
https://files.lolisafe.moe/yourFile.jpg https://files.lolisafe.moe/yourFile.jpg
Both cases require you to type the domain where the files will be served on the `domain` key below. Both cases require you to type the domain where the files will be served on the `domain` key below.
Which one you use is ultimately up to you. Which one you use is ultimately up to you.
*/ */
serveFilesWithNode: false, serveFilesWithNode: false,
domain: 'https://lolisafe.moe', domain: 'https://lolisafe.moe',
// Port on which to run the server // Port on which to run the server
port: 9999, port: 9999,
// Pages to process for the frontend // Pages to process for the frontend
pages: ['home', 'auth', 'dashboard', 'faq'], pages: ['home', 'auth', 'dashboard', 'faq'],
// Add file extensions here which should be blocked // Add file extensions here which should be blocked
blockedExtensions: [ blockedExtensions: [
'.exe', '.exe',
'.bat', '.bat',
'.cmd', '.cmd',
'.msi', '.msi',
'.sh' '.sh'
], ],
// Uploads config // Uploads config
uploads: { uploads: {
// Folder where images should be stored // Folder where images should be stored
folder: 'uploads', folder: 'uploads',
/* /*
Max file size allowed. Needs to be in MB Max file size allowed. Needs to be in MB
Note: When maxSize is greater than 1 MiB, you must set the client_max_body_size to the same as maxSize. Note: When maxSize is greater than 1 MiB, you must set the client_max_body_size to the same as maxSize.
*/ */
maxSize: '512MB', maxSize: '512MB',
// The length of the random generated name for the uploaded files // The length of the random generated name for the uploaded files
fileLength: 32, fileLength: 32,
/* /*
NOTE: Thumbnails are only for the admin panel and they require you NOTE: Thumbnails are only for the admin panel and they require you
to install a separate binary called graphicsmagick (http://www.graphicsmagick.org) to install a separate binary called graphicsmagick (http://www.graphicsmagick.org)
for images and ffmpeg (https://ffmpeg.org/) for video files for images and ffmpeg (https://ffmpeg.org/) for video files
*/ */
generateThumbnails: false, generateThumbnails: false,
/* /*
Allows users to download a .zip file of all files in an album. Allows users to download a .zip file of all files in an album.
The file is generated when the user clicks the download button in the view The file is generated when the user clicks the download button in the view
and is re-used if the album has not changed between download requests and is re-used if the album has not changed between download requests
*/ */
generateZips: true generateZips: true
}, },
// Folder where to store logs // Folder where to store logs
logsFolder: 'logs', logsFolder: 'logs',
// The following values shouldn't be touched // The following values shouldn't be touched
database: { database: {
client: 'sqlite3', client: 'sqlite3',
connection: { filename: './database/db' }, connection: { filename: './database/db' },
useNullAsDefault: true useNullAsDefault: true
} }
}; }

View File

@ -1,179 +1,178 @@
const config = require('../config.js'); const config = require('../config.js')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const randomstring = require('randomstring'); const randomstring = require('randomstring')
const utils = require('./utilsController.js'); const utils = require('./utilsController.js')
const path = require('path'); const path = require('path')
const fs = require('fs'); const fs = require('fs')
const Zip = require('jszip'); const Zip = require('jszip')
const albumsController = {}; const albumsController = {}
albumsController.list = async (req, res, next) => { albumsController.list = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const fields = ['id', 'name']; const fields = ['id', 'name']
if (req.params.sidebar === undefined) { if (req.params.sidebar === undefined) {
fields.push('timestamp'); fields.push('timestamp')
fields.push('identifier'); fields.push('identifier')
} }
const albums = await db.table('albums').select(fields).where({ enabled: 1, userid: user.id }); const albums = await db.table('albums').select(fields).where({ enabled: 1, userid: user.id })
if (req.params.sidebar !== undefined) { if (req.params.sidebar !== undefined) {
return res.json({ success: true, albums }); return res.json({ success: true, albums })
} }
let ids = []; let ids = []
for (let album of albums) { for (let album of albums) {
album.date = new Date(album.timestamp * 1000) album.date = new Date(album.timestamp * 1000)
album.date = utils.getPrettyDate(album.date) album.date = utils.getPrettyDate(album.date)
album.identifier = `${config.domain}/a/${album.identifier}`; album.identifier = `${config.domain}/a/${album.identifier}`
ids.push(album.id); ids.push(album.id)
} }
const files = await db.table('files').whereIn('albumid', ids).select('albumid'); const files = await db.table('files').whereIn('albumid', ids).select('albumid')
const albumsCount = {}; const albumsCount = {}
for (let id of ids) albumsCount[id] = 0; for (let id of ids) albumsCount[id] = 0
for (let file of files) albumsCount[file.albumid] += 1; for (let file of files) albumsCount[file.albumid] += 1
for (let album of albums) album.files = albumsCount[album.id]; for (let album of albums) album.files = albumsCount[album.id]
return res.json({ success: true, albums }); return res.json({ success: true, albums })
}; }
albumsController.create = async (req, res, next) => { albumsController.create = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const name = req.body.name; const name = req.body.name
if (name === undefined || name === '') { if (name === undefined || name === '') {
return res.json({ success: false, description: 'No album name specified' }); return res.json({ success: false, description: 'No album name specified' })
} }
const album = await db.table('albums').where({ const album = await db.table('albums').where({
name: name, name: name,
enabled: 1, enabled: 1,
userid: user.id userid: user.id
}).first(); }).first()
if (album) { if (album) {
return res.json({ success: false, description: 'There\'s already an album with that name' }) return res.json({ success: false, description: 'There\'s already an album with that name' })
} }
await db.table('albums').insert({ await db.table('albums').insert({
name: name, name: name,
enabled: 1, enabled: 1,
userid: user.id, userid: user.id,
identifier: randomstring.generate(8), identifier: randomstring.generate(8),
timestamp: Math.floor(Date.now() / 1000) timestamp: Math.floor(Date.now() / 1000)
}); })
return res.json({ success: true }); return res.json({ success: true })
}; }
albumsController.delete = async (req, res, next) => { albumsController.delete = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const id = req.body.id; const id = req.body.id
if (id === undefined || id === '') { if (id === undefined || id === '') {
return res.json({ success: false, description: 'No album specified' }); return res.json({ success: false, description: 'No album specified' })
} }
await db.table('albums').where({ id: id, userid: user.id }).update({ enabled: 0 }); await db.table('albums').where({ id: id, userid: user.id }).update({ enabled: 0 })
return res.json({ success: true }); return res.json({ success: true })
}; }
albumsController.rename = async (req, res, next) => { albumsController.rename = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const id = req.body.id; const id = req.body.id
if (id === undefined || id === '') { if (id === undefined || id === '') {
return res.json({ success: false, description: 'No album specified' }); return res.json({ success: false, description: 'No album specified' })
} }
const name = req.body.name; const name = req.body.name
if (name === undefined || name === '') { if (name === undefined || name === '') {
return res.json({ success: false, description: 'No name specified' }); return res.json({ success: false, description: 'No name specified' })
} }
const album = await db.table('albums').where({ name: name, userid: user.id }).first(); const album = await db.table('albums').where({ name: name, userid: user.id }).first()
if (album) { if (album) {
return res.json({ success: false, description: 'Name already in use' }) return res.json({ success: false, description: 'Name already in use' })
} }
await db.table('albums').where({ id: id, userid: user.id }).update({ name: name }) await db.table('albums').where({ id: id, userid: user.id }).update({ name: name })
return res.json({ success: true }); return res.json({ success: true })
}; }
albumsController.get = async (req, res, next) => { albumsController.get = async (req, res, next) => {
const identifier = req.params.identifier; const identifier = req.params.identifier
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' }); if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' })
const album = await db.table('albums').where({ identifier, enabled: 1 }).first(); const album = await db.table('albums').where({ identifier, enabled: 1 }).first()
if (!album) return res.json({ success: false, description: 'Album not found' }); if (!album) return res.json({ success: false, description: 'Album not found' })
const title = album.name; const title = album.name
const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC'); const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC')
for (let file of files) { for (let file of files) {
file.file = `${config.domain}/${file.name}`; file.file = `${config.domain}/${file.name}`
const ext = path.extname(file.name).toLowerCase(); const ext = path.extname(file.name).toLowerCase()
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) { if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${config.domain}/thumbs/${file.name.slice(0, -ext.length)}.png`; file.thumb = `${config.domain}/thumbs/${file.name.slice(0, -ext.length)}.png`
} }
} }
return res.json({
success: true,
title: title,
count: files.length,
files
});
};
return res.json({
success: true,
title: title,
count: files.length,
files
})
}
albumsController.generateZip = async (req, res, next) => { albumsController.generateZip = async (req, res, next) => {
const identifier = req.params.identifier; const identifier = req.params.identifier
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' }); if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' })
if (!config.uploads.generateZips) return res.status(401).json({ success: false, description: 'Zip generation disabled' }); if (!config.uploads.generateZips) return res.status(401).json({ success: false, description: 'Zip generation disabled' })
const album = await db.table('albums').where({ identifier, enabled: 1 }).first(); const album = await db.table('albums').where({ identifier, enabled: 1 }).first()
if (!album) return res.json({ success: false, description: 'Album not found' }); if (!album) return res.json({ success: false, description: 'Album not found' })
if (album.zipGeneratedAt > album.editedAt) { if (album.zipGeneratedAt > album.editedAt) {
const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`); const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`)
const fileName = `${album.name}.zip`; const fileName = `${album.name}.zip`
return res.download(filePath, fileName); return res.download(filePath, fileName)
} else { } else {
console.log(`Generating zip for album identifier: ${identifier}`); console.log(`Generating zip for album identifier: ${identifier}`)
const files = await db.table('files').select('name').where('albumid', album.id); const files = await db.table('files').select('name').where('albumid', album.id)
if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album' }); if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album' })
const zipPath = path.join(__dirname, '..', config.uploads.folder, 'zips', `${album.identifier}.zip`); const zipPath = path.join(__dirname, '..', config.uploads.folder, 'zips', `${album.identifier}.zip`)
let archive = new Zip(); let archive = new Zip()
for (let file of files) { for (let file of files) {
try { try {
const exists = fs.statSync(path.join(__dirname, '..', config.uploads.folder, file.name)); // const exists = fs.statSync(path.join(__dirname, '..', config.uploads.folder, file.name))
archive.file(file.name, fs.readFileSync(path.join(__dirname, '..', config.uploads.folder, file.name))); archive.file(file.name, fs.readFileSync(path.join(__dirname, '..', config.uploads.folder, file.name)))
} catch (err) { } catch (err) {
console.log(err); console.log(err)
} }
} }
archive archive
.generateNodeStream({ type: 'nodebuffer', streamFiles: true }) .generateNodeStream({ type: 'nodebuffer', streamFiles: true })
.pipe(fs.createWriteStream(zipPath)) .pipe(fs.createWriteStream(zipPath))
.on('finish', async () => { .on('finish', async () => {
console.log(`Generated zip for album identifier: ${identifier}`); console.log(`Generated zip for album identifier: ${identifier}`)
await db.table('albums') await db.table('albums')
.where('id', album.id) .where('id', album.id)
.update({ zipGeneratedAt: Math.floor(Date.now() / 1000) }); .update({ zipGeneratedAt: Math.floor(Date.now() / 1000) })
const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`); const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`)
const fileName = `${album.name}.zip`; const fileName = `${album.name}.zip`
return res.download(filePath, fileName); return res.download(filePath, fileName)
}); })
} }
}; }
module.exports = albumsController; module.exports = albumsController

View File

@ -1,86 +1,86 @@
const config = require('../config.js'); const config = require('../config.js')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt')
const randomstring = require('randomstring'); const randomstring = require('randomstring')
const utils = require('./utilsController.js'); const utils = require('./utilsController.js')
let authController = {}; let authController = {}
authController.verify = async (req, res, next) => { authController.verify = async (req, res, next) => {
const username = req.body.username; const username = req.body.username
const password = req.body.password; const password = req.body.password
if (username === undefined) return res.json({ success: false, description: 'No username provided' }); if (username === undefined) return res.json({ success: false, description: 'No username provided' })
if (password === undefined) return res.json({ success: false, description: 'No password provided' }); if (password === undefined) return res.json({ success: false, description: 'No password provided' })
const user = await db.table('users').where('username', username).first(); 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' })
bcrypt.compare(password, user.password, (err, result) => { bcrypt.compare(password, user.password, (err, result) => {
if (err) { if (err) {
console.log(err); console.log(err)
return res.json({ success: false, description: 'There was an error' }); return res.json({ success: false, description: 'There was an error' })
} }
if (result === false) return res.json({ success: false, description: 'Wrong password' }); if (result === false) return res.json({ success: false, description: 'Wrong password' })
return res.json({ success: true, token: user.token }); return res.json({ success: true, token: user.token })
}); })
}; }
authController.register = async (req, res, next) => { authController.register = async (req, res, next) => {
if (config.enableUserAccounts === false) { if (config.enableUserAccounts === false) {
return res.json({ success: false, description: 'Register is disabled at the moment' }); return res.json({ success: false, description: 'Register is disabled at the moment' })
} }
const username = req.body.username; const username = req.body.username
const password = req.body.password; const password = req.body.password
if (username === undefined) return res.json({ success: false, description: 'No username provided' }); if (username === undefined) return res.json({ success: false, description: 'No username provided' })
if (password === undefined) return res.json({ success: false, description: 'No password provided' }); if (password === undefined) return res.json({ success: false, description: 'No password provided' })
if (username.length < 4 || username.length > 32) { if (username.length < 4 || username.length > 32) {
return res.json({ success: false, description: 'Username must have 4-32 characters' }) return res.json({ success: false, description: 'Username must have 4-32 characters' })
} }
if (password.length < 6 || password.length > 64) { if (password.length < 6 || password.length > 64) {
return res.json({ success: false, description: 'Password must have 6-64 characters' }) return res.json({ success: false, description: 'Password must have 6-64 characters' })
} }
const user = await db.table('users').where('username', username).first(); const user = await db.table('users').where('username', username).first()
if (user) return res.json({ success: false, description: 'Username already exists' }); if (user) return res.json({ success: false, description: 'Username already exists' })
bcrypt.hash(password, 10, async (err, hash) => { bcrypt.hash(password, 10, async (err, hash) => {
if (err) { if (err) {
console.log(err); console.log(err)
return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' }); return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
} }
const token = randomstring.generate(64); const token = randomstring.generate(64)
await db.table('users').insert({ await db.table('users').insert({
username: username, username: username,
password: hash, password: hash,
token: token token: token
}); })
return res.json({ success: true, token: token }) return res.json({ success: true, token: token })
}); })
}; }
authController.changePassword = async (req, res, next) => { authController.changePassword = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
let password = req.body.password; let password = req.body.password
if (password === undefined) return res.json({ success: false, description: 'No password provided' }); if (password === undefined) return res.json({ success: false, description: 'No password provided' })
if (password.length < 6 || password.length > 64) { if (password.length < 6 || password.length > 64) {
return res.json({ success: false, description: 'Password must have 6-64 characters' }); return res.json({ success: false, description: 'Password must have 6-64 characters' })
} }
bcrypt.hash(password, 10, async (err, hash) => { bcrypt.hash(password, 10, async (err, hash) => {
if (err) { if (err) {
console.log(err); console.log(err)
return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' }); return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
} }
await db.table('users').where('id', user.id).update({ password: hash }); await db.table('users').where('id', user.id).update({ password: hash })
return res.json({ success: true }); return res.json({ success: true })
}); })
}; }
module.exports = authController; module.exports = authController

View File

@ -1,34 +1,34 @@
const config = require('../config.js'); const config = require('../config.js')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const randomstring = require('randomstring'); const randomstring = require('randomstring')
const utils = require('./utilsController.js'); const utils = require('./utilsController.js')
const tokenController = {}; const tokenController = {}
tokenController.verify = async (req, res, next) => { tokenController.verify = async (req, res, next) => {
const token = req.body.token; 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(); const user = await db.table('users').where('token', token).first()
if (!user) return res.status(401).json({ success: false, description: 'Invalid token' }); if (!user) return res.status(401).json({ success: false, description: 'Invalid token' })
return res.json({ success: true, username: user.username }); return res.json({ success: true, username: user.username })
}; }
tokenController.list = async (req, res, next) => { tokenController.list = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
return res.json({ success: true, token: user.token }); return res.json({ success: true, token: user.token })
}; }
tokenController.change = async (req, res, next) => { tokenController.change = async (req, res, next) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const newtoken = randomstring.generate(64); const newtoken = randomstring.generate(64)
await db.table('users').where('token', user.token).update({ await db.table('users').where('token', user.token).update({
token: newtoken, token: newtoken,
timestamp: Math.floor(Date.now() / 1000) timestamp: Math.floor(Date.now() / 1000)
}); })
res.json({ success: true, token: newtoken }); res.json({ success: true, token: newtoken })
}; }
module.exports = tokenController; module.exports = tokenController

View File

@ -1,285 +1,285 @@
const config = require('../config.js'); const config = require('../config.js')
const path = require('path'); const path = require('path')
const multer = require('multer'); const multer = require('multer')
const randomstring = require('randomstring'); const randomstring = require('randomstring')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const crypto = require('crypto'); const crypto = require('crypto')
const fs = require('fs'); const fs = require('fs')
const utils = require('./utilsController.js'); const utils = require('./utilsController.js')
const uploadsController = {}; const uploadsController = {}
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: function(req, file, cb) { destination: function (req, file, cb) {
cb(null, path.join(__dirname, '..', config.uploads.folder)); cb(null, path.join(__dirname, '..', config.uploads.folder))
}, },
filename: function(req, file, cb) { filename: function (req, file, cb) {
cb(null, randomstring.generate(config.uploads.fileLength) + path.extname(file.originalname)); cb(null, randomstring.generate(config.uploads.fileLength) + path.extname(file.originalname))
} }
}); })
const upload = multer({ const upload = multer({
storage: storage, storage: storage,
limits: { fileSize: config.uploads.maxSize }, limits: { fileSize: config.uploads.maxSize },
fileFilter: function(req, file, cb) { fileFilter: function (req, file, cb) {
if (config.blockedExtensions !== undefined) { if (config.blockedExtensions !== undefined) {
if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) { if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) {
return cb('This file extension is not allowed'); return cb('This file extension is not allowed') // eslint-disable-line standard/no-callback-literal
} }
return cb(null, true); return cb(null, true)
} }
return cb(null, true); return cb(null, true)
} }
}).array('files[]'); }).array('files[]')
uploadsController.upload = async (req, res, next) => { uploadsController.upload = async (req, res, next) => {
if (config.private === true) { if (config.private === true) {
await utils.authorize(req, res); await utils.authorize(req, res)
} }
const token = req.headers.token || ''; const token = req.headers.token || ''
const user = await db.table('users').where('token', token).first(); const user = await db.table('users').where('token', token).first()
const albumid = req.headers.albumid || req.params.albumid; const albumid = req.headers.albumid || req.params.albumid
if (albumid && user) { if (albumid && user) {
const album = await db.table('albums').where({ id: albumid, userid: user.id }).first(); const album = await db.table('albums').where({ id: albumid, userid: user.id }).first()
if (!album) { if (!album) {
return res.json({ return res.json({
success: false, success: false,
description: 'Album doesn\'t exist or it doesn\'t belong to the user' description: 'Album doesn\'t exist or it doesn\'t belong to the user'
}); })
} }
return uploadsController.actuallyUpload(req, res, user, albumid); return uploadsController.actuallyUpload(req, res, user, albumid)
} }
return uploadsController.actuallyUpload(req, res, user, albumid); return uploadsController.actuallyUpload(req, res, user, albumid)
}; }
uploadsController.actuallyUpload = async (req, res, userid, album) => { uploadsController.actuallyUpload = async (req, res, userid, album) => {
upload(req, res, async err => { upload(req, res, async err => {
if (err) { if (err) {
console.error(err); console.error(err)
return res.json({ success: false, description: err }); return res.json({ success: false, description: err })
} }
if (req.files.length === 0) return res.json({ success: false, description: 'no-files' }); if (req.files.length === 0) return res.json({ success: false, description: 'no-files' })
const files = []; const files = []
const existingFiles = []; const existingFiles = []
let iteration = 1; let iteration = 1
req.files.forEach(async file => { req.files.forEach(async file => {
// Check if the file exists by checking hash and size // Check if the file exists by checking hash and size
let hash = crypto.createHash('md5'); let hash = crypto.createHash('md5')
let stream = fs.createReadStream(path.join(__dirname, '..', config.uploads.folder, file.filename)); let stream = fs.createReadStream(path.join(__dirname, '..', config.uploads.folder, file.filename))
stream.on('data', data => { stream.on('data', data => {
hash.update(data, 'utf8'); hash.update(data, 'utf8')
}); })
stream.on('end', async () => { stream.on('end', async () => {
const fileHash = hash.digest('hex'); const fileHash = hash.digest('hex')
const dbFile = await db.table('files') const dbFile = await db.table('files')
.where(function() { .where(function () {
if (userid === undefined) this.whereNull('userid'); if (userid === undefined) this.whereNull('userid')
else this.where('userid', userid.id); else this.where('userid', userid.id)
}) })
.where({ .where({
hash: fileHash, hash: fileHash,
size: file.size size: file.size
}) })
.first(); .first()
if (!dbFile) { if (!dbFile) {
files.push({ files.push({
name: file.filename, name: file.filename,
original: file.originalname, original: file.originalname,
type: file.mimetype, type: file.mimetype,
size: file.size, size: file.size,
hash: fileHash, hash: fileHash,
ip: req.ip, ip: req.ip,
albumid: album, albumid: album,
userid: userid.id, userid: userid ? userid.id : null,
timestamp: Math.floor(Date.now() / 1000) timestamp: Math.floor(Date.now() / 1000)
}); })
} else { } else {
uploadsController.deleteFile(file.filename).then(() => {}).catch(err => console.error(err)); uploadsController.deleteFile(file.filename).then(() => {}).catch(err => console.error(err))
existingFiles.push(dbFile); existingFiles.push(dbFile)
} }
if (iteration === req.files.length) { if (iteration === req.files.length) {
return uploadsController.processFilesForDisplay(req, res, files, existingFiles); return uploadsController.processFilesForDisplay(req, res, files, existingFiles)
} }
iteration++; iteration++
}); })
}); })
}); })
}; }
uploadsController.processFilesForDisplay = async (req, res, files, existingFiles) => { uploadsController.processFilesForDisplay = async (req, res, files, existingFiles) => {
let basedomain = config.domain; let basedomain = config.domain
if (files.length === 0) { if (files.length === 0) {
return res.json({ return res.json({
success: true, success: true,
files: existingFiles.map(file => { files: existingFiles.map(file => {
return { return {
name: file.name, name: file.name,
size: file.size, size: file.size,
url: `${basedomain}/${file.name}` url: `${basedomain}/${file.name}`
}; }
}) })
}); })
} }
await db.table('files').insert(files); await db.table('files').insert(files)
for (let efile of existingFiles) files.push(efile); for (let efile of existingFiles) files.push(efile)
res.json({ res.json({
success: true, success: true,
files: files.map(file => { files: files.map(file => {
return { return {
name: file.name, name: file.name,
size: file.size, size: file.size,
url: `${basedomain}/${file.name}` url: `${basedomain}/${file.name}`
}; }
}) })
}); })
for (let file of files) { for (let file of files) {
let ext = path.extname(file.name).toLowerCase(); let ext = path.extname(file.name).toLowerCase()
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) { if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`; file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`
utils.generateThumbs(file); utils.generateThumbs(file)
} }
if (file.albumid) { if (file.albumid) {
db.table('albums').where('id', file.albumid).update('editedAt', file.timestamp).then(() => {}) db.table('albums').where('id', file.albumid).update('editedAt', file.timestamp).then(() => {})
.catch(error => { console.log(error); res.json({ success: false, description: 'Error updating album' }); }); .catch(error => { console.log(error); res.json({ success: false, description: 'Error updating album' }) })
} }
} }
}; }
uploadsController.delete = async (req, res) => { uploadsController.delete = async (req, res) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
const id = req.body.id; const id = req.body.id
if (id === undefined || id === '') { if (id === undefined || id === '') {
return res.json({ success: false, description: 'No file specified' }); return res.json({ success: false, description: 'No file specified' })
} }
const file = await db.table('files') const file = await db.table('files')
.where('id', id) .where('id', id)
.where(function() { .where(function () {
if (user.username !== 'root') { if (user.username !== 'root') {
this.where('userid', user.id); this.where('userid', user.id)
} }
}) })
.first(); .first()
try { try {
await uploadsController.deleteFile(file.name); await uploadsController.deleteFile(file.name)
await db.table('files').where('id', id).del(); await db.table('files').where('id', id).del()
if (file.albumid) { if (file.albumid) {
await db.table('albums').where('id', file.albumid).update('editedAt', Math.floor(Date.now() / 1000)); await db.table('albums').where('id', file.albumid).update('editedAt', Math.floor(Date.now() / 1000))
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err)
} }
return res.json({ success: true }); return res.json({ success: true })
}; }
uploadsController.deleteFile = function(file) { uploadsController.deleteFile = function (file) {
const ext = path.extname(file).toLowerCase(); const ext = path.extname(file).toLowerCase()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path.join(__dirname, '..', config.uploads.folder, file), (err, stats) => { fs.stat(path.join(__dirname, '..', config.uploads.folder, file), (err, stats) => {
if (err) { return reject(err); } if (err) { return reject(err) }
fs.unlink(path.join(__dirname, '..', config.uploads.folder, file), err => { fs.unlink(path.join(__dirname, '..', config.uploads.folder, file), err => {
if (err) { return reject(err); } if (err) { return reject(err) }
if (!utils.imageExtensions.includes(ext) && !utils.videoExtensions.includes(ext)) { if (!utils.imageExtensions.includes(ext) && !utils.videoExtensions.includes(ext)) {
return resolve(); return resolve()
} }
file = file.substr(0, file.lastIndexOf('.')) + '.png'; file = file.substr(0, file.lastIndexOf('.')) + '.png'
fs.stat(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), (err, stats) => { fs.stat(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), (err, stats) => {
if (err) { if (err) {
console.log(err); console.log(err)
return resolve(); return resolve()
} }
fs.unlink(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), err => { fs.unlink(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), err => {
if (err) { return reject(err); } if (err) { return reject(err) }
return resolve(); return resolve()
}); })
}); })
}); })
}); })
}); })
}; }
uploadsController.list = async (req, res) => { uploadsController.list = async (req, res) => {
const user = await utils.authorize(req, res); const user = await utils.authorize(req, res)
let offset = req.params.page; let offset = req.params.page
if (offset === undefined) offset = 0; if (offset === undefined) offset = 0
const files = await db.table('files') const files = await db.table('files')
.where(function() { .where(function () {
if (req.params.id === undefined) this.where('id', '<>', ''); if (req.params.id === undefined) this.where('id', '<>', '')
else this.where('albumid', req.params.id); else this.where('albumid', req.params.id)
}) })
.where(function() { .where(function () {
if (user.username !== 'root') this.where('userid', user.id); if (user.username !== 'root') this.where('userid', user.id)
}) })
.orderBy('id', 'DESC') .orderBy('id', 'DESC')
.limit(25) .limit(25)
.offset(25 * offset) .offset(25 * offset)
.select('id', 'albumid', 'timestamp', 'name', 'userid'); .select('id', 'albumid', 'timestamp', 'name', 'userid')
const albums = await db.table('albums'); const albums = await db.table('albums')
let basedomain = config.domain; let basedomain = config.domain
let userids = []; let userids = []
for (let file of files) { for (let file of files) {
file.file = `${basedomain}/${file.name}`; file.file = `${basedomain}/${file.name}`
file.date = new Date(file.timestamp * 1000); file.date = new Date(file.timestamp * 1000)
file.date = utils.getPrettyDate(file.date); file.date = utils.getPrettyDate(file.date)
file.album = ''; file.album = ''
if (file.albumid !== undefined) { if (file.albumid !== undefined) {
for (let album of albums) { for (let album of albums) {
if (file.albumid === album.id) { if (file.albumid === album.id) {
file.album = album.name; file.album = album.name
} }
} }
} }
// Only push usernames if we are root // Only push usernames if we are root
if (user.username === 'root') { if (user.username === 'root') {
if (file.userid !== undefined && file.userid !== null && file.userid !== '') { if (file.userid !== undefined && file.userid !== null && file.userid !== '') {
userids.push(file.userid); userids.push(file.userid)
} }
} }
let ext = path.extname(file.name).toLowerCase(); let ext = path.extname(file.name).toLowerCase()
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) { if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`; file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`
} }
} }
// If we are a normal user, send response // If we are a normal user, send response
if (user.username !== 'root') return res.json({ success: true, files }); if (user.username !== 'root') return res.json({ success: true, files })
// If we are root but there are no uploads attached to a user, send response // If we are root but there are no uploads attached to a user, send response
if (userids.length === 0) return res.json({ success: true, files }); 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 (let dbUser of users) { for (let dbUser of users) {
for (let file of files) { for (let file of files) {
if (file.userid === dbUser.id) { if (file.userid === dbUser.id) {
file.username = dbUser.username; file.username = dbUser.username
} }
} }
} }
return res.json({ success: true, files }); return res.json({ success: true, files })
}; }
module.exports = uploadsController; module.exports = uploadsController

View File

@ -1,67 +1,67 @@
const path = require('path'); const path = require('path')
const config = require('../config.js'); const config = require('../config.js')
const fs = require('fs'); const fs = require('fs')
const gm = require('gm'); const gm = require('gm')
const ffmpeg = require('fluent-ffmpeg'); const ffmpeg = require('fluent-ffmpeg')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const utilsController = {}; const utilsController = {}
utilsController.imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png']; utilsController.imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png']
utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']
utilsController.getPrettyDate = function(date) { utilsController.getPrettyDate = function (date) {
return date.getFullYear() + '-' return date.getFullYear() + '-' +
+ (date.getMonth() + 1) + '-' (date.getMonth() + 1) + '-' +
+ date.getDate() + ' ' date.getDate() + ' ' +
+ (date.getHours() < 10 ? '0' : '') (date.getHours() < 10 ? '0' : '') +
+ date.getHours() + ':' date.getHours() + ':' +
+ (date.getMinutes() < 10 ? '0' : '') (date.getMinutes() < 10 ? '0' : '') +
+ date.getMinutes() + ':' date.getMinutes() + ':' +
+ (date.getSeconds() < 10 ? '0' : '') (date.getSeconds() < 10 ? '0' : '') +
+ date.getSeconds(); date.getSeconds()
} }
utilsController.authorize = async (req, res) => { utilsController.authorize = async (req, res) => {
const token = req.headers.token; const token = req.headers.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(); const user = await db.table('users').where('token', token).first()
if (!user) return res.status(401).json({ success: false, description: 'Invalid token' }); if (!user) return res.status(401).json({ success: false, description: 'Invalid token' })
return user; return user
}; }
utilsController.generateThumbs = function(file, basedomain) { utilsController.generateThumbs = function (file, basedomain) {
if (config.uploads.generateThumbnails !== true) return; if (config.uploads.generateThumbnails !== true) return
const ext = path.extname(file.name).toLowerCase(); const ext = path.extname(file.name).toLowerCase()
let thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png'); let thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png')
fs.access(thumbname, err => { fs.access(thumbname, err => {
if (err && err.code === 'ENOENT') { if (err && err.code === 'ENOENT') {
if (utilsController.videoExtensions.includes(ext)) { if (utilsController.videoExtensions.includes(ext)) {
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name)) ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
.thumbnail({ .thumbnail({
timestamps: [0], timestamps: [0],
filename: '%b.png', filename: '%b.png',
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'), folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
size: '200x?' size: '200x?'
}) })
.on('error', error => console.log('Error - ', error.message)); .on('error', error => console.log('Error - ', error.message))
} else { } else {
let size = { let size = {
width: 200, width: 200,
height: 200 height: 200
}; }
gm(path.join(__dirname, '..', config.uploads.folder, file.name)) gm(path.join(__dirname, '..', config.uploads.folder, file.name))
.resize(size.width, size.height + '>') .resize(size.width, size.height + '>')
.gravity('Center') .gravity('Center')
.extent(size.width, size.height) .extent(size.width, size.height)
.background('transparent') .background('transparent')
.write(thumbname, error => { .write(thumbname, error => {
if (error) console.log('Error - ', error); if (error) console.log('Error - ', error)
}); })
} }
} }
}); })
}; }
module.exports = utilsController; module.exports = utilsController

View File

@ -1,50 +1,49 @@
let init = function(db){ let init = function (db) {
// Create the tables we need to store galleries and files
db.schema.createTableIfNotExists('albums', function (table) {
table.increments()
table.integer('userid')
table.string('name')
table.string('identifier')
table.integer('enabled')
table.integer('timestamp')
}).then(() => {})
// Create the tables we need to store galleries and files db.schema.createTableIfNotExists('files', function (table) {
db.schema.createTableIfNotExists('albums', function (table) { table.increments()
table.increments() table.integer('userid')
table.integer('userid') table.string('name')
table.string('name') table.string('original')
table.string('identifier') table.string('type')
table.integer('enabled') table.string('size')
table.integer('timestamp') table.string('hash')
}).then(() => {}) table.string('ip')
table.integer('albumid')
table.integer('timestamp')
}).then(() => {})
db.schema.createTableIfNotExists('files', function (table) { db.schema.createTableIfNotExists('users', function (table) {
table.increments() table.increments()
table.integer('userid') table.string('username')
table.string('name') table.string('password')
table.string('original') table.string('token')
table.string('type') table.integer('timestamp')
table.string('size') }).then(() => {
table.string('hash') db.table('users').where({username: 'root'}).then((user) => {
table.string('ip') if (user.length > 0) return
table.integer('albumid')
table.integer('timestamp')
}).then(() => {})
db.schema.createTableIfNotExists('users', function (table) { require('bcrypt').hash('root', 10, function (err, hash) {
table.increments() if (err) console.error('Error generating password hash for root')
table.string('username')
table.string('password')
table.string('token')
table.integer('timestamp')
}).then(() => {
db.table('users').where({username: 'root'}).then((user) => {
if(user.length > 0) return
require('bcrypt').hash('root', 10, function(err, hash) { db.table('users').insert({
if(err) console.error('Error generating password hash for root') username: 'root',
password: hash,
db.table('users').insert({ token: require('randomstring').generate(64),
username: 'root', timestamp: Math.floor(Date.now() / 1000)
password: hash, }).then(() => {})
token: require('randomstring').generate(64), })
timestamp: Math.floor(Date.now() / 1000) })
}).then(() => {}) })
})
})
})
} }
module.exports = init module.exports = init

View File

@ -1,13 +1,13 @@
const config = require('../config.js'); const config = require('../config.js')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const migration = {}; const migration = {}
migration.start = async () => { migration.start = async () => {
await db.schema.table('albums', table => { await db.schema.table('albums', table => {
table.dateTime('editedAt'); table.dateTime('editedAt')
table.dateTime('zipGeneratedAt'); table.dateTime('zipGeneratedAt')
}); })
console.log('Migration finished! Now start lolisafe normally'); console.log('Migration finished! Now start lolisafe normally')
}; }
migration.start(); migration.start()

View File

@ -1,58 +1,66 @@
const config = require('./config.js'); const config = require('./config.js')
const api = require('./routes/api.js'); const api = require('./routes/api.js')
const album = require('./routes/album.js'); const album = require('./routes/album.js')
const express = require('express'); const express = require('express')
const helmet = require('helmet'); const helmet = require('helmet')
const bodyParser = require('body-parser'); const bodyParser = require('body-parser')
const RateLimit = require('express-rate-limit'); const RateLimit = require('express-rate-limit')
const db = require('knex')(config.database); const db = require('knex')(config.database)
const fs = require('fs'); const fs = require('fs')
const exphbs = require('express-handlebars'); const exphbs = require('express-handlebars')
const safe = express(); const safe = express()
require('./database/db.js')(db); require('./database/db.js')(db)
fs.existsSync('./pages/custom') || fs.mkdirSync('./pages/custom'); fs.existsSync('./pages/custom') || fs.mkdirSync('./pages/custom')
fs.existsSync(`./${config.logsFolder}`) || fs.mkdirSync(`./${ config.logsFolder}`); fs.existsSync('./' + config.logsFolder) || fs.mkdirSync('./' + config.logsFolder)
fs.existsSync(`./${config.uploads.folder}`) || fs.mkdirSync(`./${config.uploads.folder}`); fs.existsSync('./' + config.uploads.folder) || fs.mkdirSync('./' + config.uploads.folder)
fs.existsSync(`./${config.uploads.folder}/thumbs`) || fs.mkdirSync(`./${config.uploads.folder }/thumbs`); fs.existsSync('./' + config.uploads.folder + '/thumbs') || fs.mkdirSync('./' + config.uploads.folder + '/thumbs')
fs.existsSync(`./${config.uploads.folder }/zips`) || fs.mkdirSync(`./${config.uploads.folder}/zips`); fs.existsSync('./' + config.uploads.folder + '/zips') || fs.mkdirSync('./' + config.uploads.folder + '/zips')
safe.use(helmet()); safe.use(helmet())
safe.set('trust proxy', 1); safe.set('trust proxy', 1)
safe.engine('handlebars', exphbs({ defaultLayout: 'main' })); safe.engine('handlebars', exphbs({ defaultLayout: 'main' }))
safe.set('view engine', 'handlebars'); safe.set('view engine', 'handlebars')
safe.enable('view cache'); safe.enable('view cache')
let limiter = new RateLimit({ windowMs: 5000, max: 2 }); let limiter = new RateLimit({ windowMs: 5000, max: 2 })
safe.use('/api/login/', limiter); safe.use('/api/login/', limiter)
safe.use('/api/register/', limiter); safe.use('/api/register/', limiter)
safe.use(bodyParser.urlencoded({ extended: true })); safe.use(bodyParser.urlencoded({ extended: true }))
safe.use(bodyParser.json()); safe.use(bodyParser.json())
if (config.serveFilesWithNode) { if (config.serveFilesWithNode) {
safe.use('/', express.static(config.uploads.folder)); safe.use('/', express.static(config.uploads.folder))
} }
safe.use('/', express.static('./public')); safe.use('/', express.static('./public'))
safe.use('/', album); safe.use('/', album)
safe.use('/api', api); safe.use('/api', api)
for (let page of config.pages) { for (let page of config.pages) {
let root = './pages/'; let root = './pages/'
if (fs.existsSync(`./pages/custom/${page}.html`)) { if (fs.existsSync(`./pages/custom/${page}.html`)) {
root = './pages/custom/'; root = './pages/custom/'
} }
if (page === 'home') { if (page === 'home') {
safe.get('/', (req, res, next) => res.sendFile(`${page}.html`, { root: root })); safe.get('/', (req, res, next) => res.sendFile(`${page}.html`, { root: root }))
} else { } else {
safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { root: root })); safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { root: root }))
} }
} }
safe.use((req, res, next) => res.status(404).sendFile('404.html', { root: './pages/error/' })); safe.use((req, res, next) => res.status(404).sendFile('404.html', { root: './pages/error/' }))
safe.use((req, res, next) => res.status(500).sendFile('500.html', { root: './pages/error/' })); safe.use((req, res, next) => res.status(500).sendFile('500.html', { root: './pages/error/' }))
safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`)); safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`))
process.on('uncaughtException', err => {
console.error(`Uncaught Exception:\n${err.stack}`)
})
process.on('unhandledRejection', err => {
console.error(`Unhandled Rejection (Promise):\n${err.stack}`)
})

View File

@ -30,19 +30,9 @@
"sqlite3": "^3.1.13" "sqlite3": "^3.1.13"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^4.4.1", "standard": "^10.0.3"
"eslint-config-aqua": "^1.5.0"
}, },
"eslintConfig": { "standard": {
"extends": [ "envs": ["browser", "node"]
"aqua"
],
"env": {
"browser": true,
"node": true
},
"rules": {
"func-names": 0
}
} }
} }

View File

@ -39,6 +39,7 @@
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script> <script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
<script type="text/javascript" src="/js/auth.js"></script> <script type="text/javascript" src="/js/auth.js"></script>
<style type="text/css"> <style type="text/css">
/** Based on KDE Breeze Dark **/ /** Based on KDE Breeze Dark **/
@ -46,6 +47,7 @@
background-color: #232629; background-color: #232629;
} }
</style> </style>
</head> </head>
<body> <body>

View File

@ -39,6 +39,7 @@
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script> <script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
<script type="text/javascript" src="/js/dashboard.js"></script> <script type="text/javascript" src="/js/dashboard.js"></script>
<style type="text/css"> <style type="text/css">
/** Based on KDE Breeze Dark **/ /** Based on KDE Breeze Dark **/
@ -122,6 +123,7 @@
border-left-color: #31363b; border-left-color: #31363b;
} }
</style> </style>
</head> </head>
<body> <body>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<browserconfig> <browserconfig>
<msapplication> <msapplication>
<tile> <tile>
<square150x150logo src="/images/icons/mstile-150x150.png?v=ZqYs7M3fG4"/> <square150x150logo src="/images/icons/mstile-150x150.png?v=ZqYs7M3fG4"/>
<TileColor>#232629</TileColor> <TileColor>#232629</TileColor>
</tile> </tile>
</msapplication> </msapplication>
</browserconfig> </browserconfig>

View File

@ -1,18 +1,18 @@
{ {
"name": "safe.fiery.me", "name": "safe.fiery.me",
"icons": [ "icons": [
{ {
"src": "/images/icons/android-chrome-192x192.png?v=ZqYs7M3fG4", "src": "/images/icons/android-chrome-192x192.png?v=ZqYs7M3fG4",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/images/icons/android-chrome-384x384.png?v=ZqYs7M3fG4", "src": "/images/icons/android-chrome-384x384.png?v=ZqYs7M3fG4",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#232629", "theme_color": "#232629",
"background_color": "#232629", "background_color": "#232629",
"display": "standalone" "display": "standalone"
} }

View File

@ -1,44 +1,56 @@
var page = {}; /* global swal, axios */
page.do = function(dest) { var page = {}
var user = document.getElementById('user').value;
var pass = document.getElementById('pass').value;
if (user === undefined || user === null || user === '') { return swal('Error', 'You need to specify a username', 'error'); } page.do = function (dest) {
if (pass === undefined || pass === null || pass === '') { return swal('Error', 'You need to specify a username', 'error'); } var user = document.getElementById('user').value
var pass = document.getElementById('pass').value
axios.post(`/api/${dest}`, { if (user === undefined || user === null || user === '') {
username: user, return swal('Error', 'You need to specify a username', 'error')
password: pass }
}) if (pass === undefined || pass === null || pass === '') {
.then(response => { return swal('Error', 'You need to specify a username', 'error')
if (response.data.success === false) { return swal('Error', response.data.description, 'error'); } }
localStorage.token = response.data.token; axios.post('/api/' + dest, {
window.location = '/dashboard'; username: user,
}) password: pass
.catch(error => { })
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error'); .then(function (response) {
console.log(error); if (response.data.success === false) {
}); return swal('Error', response.data.description, 'error')
}; }
page.verify = function() { localStorage.token = response.data.token
page.token = localStorage.token; window.location = '/dashboard'
if (page.token === undefined) return; })
.catch(function (error) {
console.log(error)
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
axios.post('/api/tokens/verify', { token: page.token }) page.verify = function () {
.then(response => { page.token = localStorage.token
if (response.data.success === false) { return swal('Error', response.data.description, 'error'); } if (page.token === undefined) return
window.location = '/dashboard'; axios.post('/api/tokens/verify', {
}) token: page.token
.catch(error => { })
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error'); .then(function (response) {
console.log(error); if (response.data.success === false) {
}); return swal('Error', response.data.description, 'error')
}; }
window.onload = function() { window.location = '/dashboard'
page.verify(); })
}; .catch(function (error) {
console.log(error)
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
window.onload = function () {
page.verify()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,172 +1,178 @@
var upload = {}; /* eslint-disable no-unused-expressions */
/* global swal, axios, Dropzone */
upload.isPrivate = true; var upload = {}
upload.token = localStorage.token;
upload.maxFileSize; upload.isPrivate = true
upload.token = localStorage.token
upload.maxFileSize
// Add the album var to the upload so we can store the album id in there // Add the album var to the upload so we can store the album id in there
upload.album; upload.album
upload.myDropzone; upload.myDropzone
upload.checkIfPublic = function() { upload.checkIfPublic = function () {
axios.get('/api/check') axios.get('/api/check')
.then(response => { .then(response => {
upload.isPrivate = response.data.private; upload.isPrivate = response.data.private
upload.maxFileSize = response.data.maxFileSize; upload.maxFileSize = response.data.maxFileSize
upload.preparePage(); upload.preparePage()
}) })
.catch(error => { .catch(error => {
swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error'); swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error')
return console.log(error); return console.log(error)
}); })
}; }
upload.preparePage = function() { upload.preparePage = function () {
if (!upload.isPrivate) return upload.prepareUpload(); if (!upload.isPrivate) return upload.prepareUpload()
if (!upload.token) return document.getElementById('loginToUpload').style.display = 'inline-flex'; if (!upload.token) {
upload.verifyToken(upload.token, true); document.getElementById('loginToUpload').style.display = 'inline-flex'
}; return 'inline-flex'
}
upload.verifyToken(upload.token, true)
}
upload.verifyToken = function(token, reloadOnError) { upload.verifyToken = function (token, reloadOnError) {
if (reloadOnError === undefined) { reloadOnError = false; } if (reloadOnError === undefined) { reloadOnError = false }
axios.post('/api/tokens/verify', { token: token }) axios.post('/api/tokens/verify', { token: token })
.then(response => { .then(response => {
if (response.data.success === false) { if (response.data.success === false) {
swal({ swal({
title: 'An error ocurred', title: 'An error ocurred',
text: response.data.description, text: response.data.description,
type: 'error' type: 'error'
}, () => { }, () => {
if (reloadOnError) { if (reloadOnError) {
localStorage.removeItem('token'); localStorage.removeItem('token')
location.reload(); location.reload()
} }
}); })
return; return
} }
localStorage.token = token; localStorage.token = token
upload.token = token; upload.token = token
return upload.prepareUpload(); return upload.prepareUpload()
}) })
.catch(error => { .catch(error => {
swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error'); swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error')
return console.log(error); return console.log(error)
}); })
}; }
upload.prepareUpload = function() { upload.prepareUpload = function () {
// I think this fits best here because we need to check for a valid token before we can get the albums // I think this fits best here because we need to check for a valid token before we can get the albums
if (upload.token) { if (upload.token) {
var select = document.getElementById('albumSelect'); var select = document.getElementById('albumSelect')
select.addEventListener('change', () => { select.addEventListener('change', () => {
upload.album = select.value; upload.album = select.value
}); })
axios.get('/api/albums', { headers: { token: upload.token } }) axios.get('/api/albums', { headers: { token: upload.token } })
.then(res => { .then(res => {
var albums = res.data.albums; var albums = res.data.albums
// If the user doesn't have any albums we don't really need to display // If the user doesn't have any albums we don't really need to display
// an album selection // an album selection
if (albums.length === 0) return; if (albums.length === 0) return
// Loop through the albums and create an option for each album // Loop through the albums and create an option for each album
for (var i = 0; i < albums.length; i++) { for (var i = 0; i < albums.length; i++) {
var opt = document.createElement('option'); var opt = document.createElement('option')
opt.value = albums[i].id; opt.value = albums[i].id
opt.innerHTML = albums[i].name; opt.innerHTML = albums[i].name
select.appendChild(opt); select.appendChild(opt)
} }
// Display the album selection // Display the album selection
document.getElementById('albumDiv').style.display = 'block'; document.getElementById('albumDiv').style.display = 'block'
}) })
.catch(e => { .catch(e => {
swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error'); swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error')
return console.log(e); return console.log(e)
}); })
} }
div = document.createElement('div'); var div = document.createElement('div')
div.id = 'dropzone'; div.id = 'dropzone'
div.innerHTML = 'Click here or drag and drop files'; div.innerHTML = 'Click here or drag and drop files'
div.style.display = 'flex'; div.style.display = 'flex'
document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${upload.maxFileSize}`; document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${upload.maxFileSize}`
document.getElementById('loginToUpload').style.display = 'none'; document.getElementById('loginToUpload').style.display = 'none'
if (upload.token === undefined) { document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads'; } if (upload.token === undefined) { document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads' }
document.getElementById('uploadContainer').appendChild(div); document.getElementById('uploadContainer').appendChild(div)
upload.prepareDropzone(); upload.prepareDropzone()
}; }
upload.prepareDropzone = function() { upload.prepareDropzone = function () {
var previewNode = document.querySelector('#template'); var previewNode = document.querySelector('#template')
previewNode.id = ''; previewNode.id = ''
var previewTemplate = previewNode.parentNode.innerHTML; var previewTemplate = previewNode.parentNode.innerHTML
previewNode.parentNode.removeChild(previewNode); previewNode.parentNode.removeChild(previewNode)
var dropzone = new Dropzone('div#dropzone', { var dropzone = new Dropzone('div#dropzone', {
url: '/api/upload', url: '/api/upload',
paramName: 'files[]', paramName: 'files[]',
maxFilesize: upload.maxFileSize.slice(0, -2), maxFilesize: upload.maxFileSize.slice(0, -2),
parallelUploads: 2, parallelUploads: 2,
uploadMultiple: false, uploadMultiple: false,
previewsContainer: 'div#uploads', previewsContainer: 'div#uploads',
previewTemplate: previewTemplate, previewTemplate: previewTemplate,
createImageThumbnails: false, createImageThumbnails: false,
maxFiles: 1000, maxFiles: 1000,
autoProcessQueue: true, autoProcessQueue: true,
headers: { token: upload.token }, headers: { token: upload.token },
init: function() { init: function () {
upload.myDropzone = this; upload.myDropzone = this
this.on('addedfile', file => { this.on('addedfile', file => {
document.getElementById('uploads').style.display = 'block'; document.getElementById('uploads').style.display = 'block'
}); })
// Add the selected albumid, if an album is selected, as a header // Add the selected albumid, if an album is selected, as a header
this.on('sending', (file, xhr) => { this.on('sending', (file, xhr) => {
if (upload.album) { if (upload.album) {
xhr.setRequestHeader('albumid', upload.album); xhr.setRequestHeader('albumid', upload.album)
} }
}); })
} }
}); })
// Update the total progress bar // Update the total progress bar
dropzone.on('uploadprogress', (file, progress) => { dropzone.on('uploadprogress', (file, progress) => {
file.previewElement.querySelector('.progress').setAttribute('value', progress); file.previewElement.querySelector('.progress').setAttribute('value', progress)
file.previewElement.querySelector('.progress').innerHTML = `${progress}%`; file.previewElement.querySelector('.progress').innerHTML = `${progress}%`
}); })
dropzone.on('success', (file, response) => { dropzone.on('success', (file, response) => {
// Handle the responseText here. For example, add the text to the preview element: // Handle the responseText here. For example, add the text to the preview element:
if (response.success === false) { if (response.success === false) {
var span = document.createElement('span'); var span = document.createElement('span')
span.innerHTML = response.description; span.innerHTML = response.description
file.previewTemplate.querySelector('.link').appendChild(span); file.previewTemplate.querySelector('.link').appendChild(span)
return; return
} }
a = document.createElement('a'); var a = document.createElement('a')
a.href = response.files[0].url; a.href = response.files[0].url
a.target = '_blank'; a.target = '_blank'
a.innerHTML = response.files[0].url; a.innerHTML = response.files[0].url
file.previewTemplate.querySelector('.link').appendChild(a); file.previewTemplate.querySelector('.link').appendChild(a)
file.previewTemplate.querySelector('.progress').style.display = 'none'; file.previewTemplate.querySelector('.progress').style.display = 'none'
}); })
upload.prepareShareX(); upload.prepareShareX()
}; }
upload.prepareShareX = function() { upload.prepareShareX = function () {
if (upload.token) { if (upload.token) {
var sharex_element = document.getElementById('ShareX'); var sharexElement = document.getElementById('ShareX')
var sharex_file = `{\r\n\ var sharexFile = `{\r\n\
"Name": "${location.hostname}",\r\n\ "Name": "${location.hostname}",\r\n\
"DestinationType": "ImageUploader, FileUploader",\r\n\ "DestinationType": "ImageUploader, FileUploader",\r\n\
"RequestType": "POST",\r\n\ "RequestType": "POST",\r\n\
@ -178,30 +184,29 @@ upload.prepareShareX = function() {
"ResponseType": "Text",\r\n\ "ResponseType": "Text",\r\n\
"URL": "$json:files[0].url$",\r\n\ "URL": "$json:files[0].url$",\r\n\
"ThumbnailURL": "$json:files[0].url$"\r\n\ "ThumbnailURL": "$json:files[0].url$"\r\n\
}`; }`
var sharex_blob = new Blob([sharex_file], { type: 'application/octet-binary' }); var sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })
sharex_element.setAttribute('href', URL.createObjectURL(sharex_blob)); sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob))
sharex_element.setAttribute('download', `${location.hostname}.sxcu`); sharexElement.setAttribute('download', `${location.hostname}.sxcu`)
} }
}; }
// Handle image paste event // Handle image paste event
window.addEventListener('paste', event => { window.addEventListener('paste', event => {
var items = (event.clipboardData || event.originalEvent.clipboardData).items; var items = (event.clipboardData || event.originalEvent.clipboardData).items
for (index in items) { for (var index in items) {
var item = items[index]; var item = items[index]
if (item.kind === 'file') { if (item.kind === 'file') {
var blob = item.getAsFile(); var blob = item.getAsFile()
console.log(blob.type); console.log(blob.type)
var file = new File([blob], `pasted-image.${blob.type.match(/(?:[^\/]*\/)([^;]*)/)[1]}`); var file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`)
file.type = blob.type; file.type = blob.type
console.log(file); console.log(file)
upload.myDropzone.addFile(file); upload.myDropzone.addFile(file)
} }
} }
}); })
window.onload = function() {
upload.checkIfPublic();
};
window.onload = function () {
upload.checkIfPublic()
}

View File

@ -1,56 +1,55 @@
const config = require('../config.js'); const config = require('../config.js')
const routes = require('express').Router(); const routes = require('express').Router()
const db = require('knex')(config.database); const db = require('knex')(config.database)
const path = require('path'); const path = require('path')
const utils = require('../controllers/utilsController.js'); const utils = require('../controllers/utilsController.js')
routes.get('/a/:identifier', async (req, res, next) => { routes.get('/a/:identifier', async (req, res, next) => {
let identifier = req.params.identifier; let identifier = req.params.identifier
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' }); if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' })
const album = await db.table('albums').where({ identifier, enabled: 1 }).first(); const album = await db.table('albums').where({ identifier, enabled: 1 }).first()
if (!album) return res.status(404).sendFile('404.html', { root: './pages/error/' }); if (!album) return res.status(404).sendFile('404.html', { root: './pages/error/' })
const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC'); const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC')
let thumb = ''; let thumb = ''
const basedomain = config.domain; const basedomain = config.domain
for (let file of files) { for (let file of files) {
file.file = `${basedomain}/${file.name}`; file.file = `${basedomain}/${file.name}`
let ext = path.extname(file.name).toLowerCase(); let ext = path.extname(file.name).toLowerCase()
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) { if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`; file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`
/* /*
If thumbnail for album is still not set, do it. If thumbnail for album is still not set, do it.
A potential improvement would be to let the user upload a specific image as an album cover A potential improvement would be to let the user upload a specific image as an album cover
since embedding the first image could potentially result in nsfw content when pasting links. since embedding the first image could potentially result in nsfw content when pasting links.
*/ */
if (thumb === '') { if (thumb === '') {
thumb = file.thumb; thumb = file.thumb
} }
file.thumb = `<img src="${file.thumb}"/>`; file.thumb = `<img src="${file.thumb}"/>`
} else { } else {
file.thumb = `<h1 class="title">.${ext}</h1>`; file.thumb = `<h1 class="title">.${ext}</h1>`
} }
} }
let enableDownload = false
if (config.uploads.generateZips) enableDownload = true
let enableDownload = false; return res.render('album', {
if (config.uploads.generateZips) enableDownload = true; layout: false,
title: album.name,
count: files.length,
thumb,
files,
identifier,
enableDownload
})
})
return res.render('album', { module.exports = routes
layout: false,
title: album.name,
count: files.length,
thumb,
files,
identifier,
enableDownload
});
});
module.exports = routes;

View File

@ -1,37 +1,37 @@
const config = require('../config.js'); const config = require('../config.js')
const routes = require('express').Router(); const routes = require('express').Router()
const uploadController = require('../controllers/uploadController'); const uploadController = require('../controllers/uploadController')
const albumsController = require('../controllers/albumsController'); const albumsController = require('../controllers/albumsController')
const tokenController = require('../controllers/tokenController'); const tokenController = require('../controllers/tokenController')
const authController = require('../controllers/authController'); const authController = require('../controllers/authController')
routes.get('/check', (req, res, next) => { routes.get('/check', (req, res, next) => {
return res.json({ return res.json({
private: config.private, private: config.private,
maxFileSize: config.uploads.maxSize maxFileSize: config.uploads.maxSize
}); })
}); })
routes.post('/login', (req, res, next) => authController.verify(req, res, next)); routes.post('/login', (req, res, next) => authController.verify(req, res, next))
routes.post('/register', (req, res, next) => authController.register(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.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', (req, res, next) => uploadController.list(req, res, next))
routes.get('/uploads/:page', (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', (req, res, next) => uploadController.upload(req, res, next))
routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next)); routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next))
routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(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/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/zip/:identifier', (req, res, next) => albumsController.generateZip(req, res, next))
routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next)); routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next))
routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next)); routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next))
routes.get('/albums', (req, res, next) => albumsController.list(req, res, next)); routes.get('/albums', (req, res, next) => albumsController.list(req, res, next))
routes.get('/albums/:sidebar', (req, res, next) => albumsController.list(req, res, next)); routes.get('/albums/:sidebar', (req, res, next) => albumsController.list(req, res, next))
routes.post('/albums', (req, res, next) => albumsController.create(req, res, next)); routes.post('/albums', (req, res, next) => albumsController.create(req, res, next))
routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next)); routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next))
routes.post('/albums/rename', (req, res, next) => albumsController.rename(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('/albums/test', (req, res, next) => albumsController.test(req, res, next))
routes.get('/tokens', (req, res, next) => tokenController.list(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/verify', (req, res, next) => tokenController.verify(req, res, next))
routes.post('/tokens/change', (req, res, next) => tokenController.change(req, res, next)); routes.post('/tokens/change', (req, res, next) => tokenController.change(req, res, next))
module.exports = routes; module.exports = routes

763
yarn.lock

File diff suppressed because it is too large Load Diff