Merged dev into master

This commit is contained in:
Pitu 2017-01-30 05:13:07 -03:00
commit 6b7fd3bcf4
19 changed files with 606 additions and 358 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ uploads/
logs/
database/db
config.js
start.json
start.json
npm-debug.log

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Pitu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,27 +1,32 @@
![loli-safe](https://a.cuntflaps.me/jcutlz.png)
# loli-safe
Pomf-like file uploading service, but way better.
A small safe worth protecting.
---
### Sites using loli-safe
- [lolisafe.moe](https://lolisafe.moe): A small safe with a smile worth protecting.
- [cuntflaps.me](https://cuntflaps.me)
- [fluntcaps.me](https://fluntcaps.me)
- Feel free to add yours here.
---
1. Clone
2. Rename `config.sample.js` to `config.js`
4. Modify port, basedomain and privacy options if desired
3. run `npm install` to install all dependencies
5. run `pm2 start lolisafe.js` or `node lolisafe.js` to start the service
### Token
This service supports running both as public and private. The only difference is that one needs a token to upload and the other one doesn't. If you want it to be public so anyone can upload files either from the website or API, just set the option `private: false` in the `config.js` file.
### Getting started
This service supports running both as public and private. The only difference is that one needs a token to upload and the other one doesn't. If you want it to be public so anyone can upload files either from the website or API, just set the option `private: false` in the `config.js` file. In case you want to run it privately, you should set `private: true`.
In case you want to run it privately, you should set `private: true` and 2 tokens will be generated when you first run the service. This tokens are a client and admin token respectively.
Upon running the service for the first time, it's gonna create a user account with the username `root` and password `root`. This is your admin account and you should change the password immediately. This account will let you manage all uploaded files and remove any if necessary.
The client token should be sent with every upload request to the service to validate an upload, making it so that only people with access to said token can upload files to the service.
The admin token is used to access the admin dashboard. This one is generated regardless of how the service is running (public or private) because it grants you access to the dashboard where you can see all uploaded files, create albums to sort out stuff and change your access tokens.
When you first run the service both tokens will be displayed on the console so you can copy and save them. Keep in mind that the tokens are nothing more than random generated strings so you can always change them for something easy to remember, acting more like a password than a token. But since the token is sent to the server with each request either to upload files or access the admin dashboard, I suggest keeping them random.
If you set `enableUserAccounts: true`, people will be able to create accounts on the service to keep track of their uploaded files and create albums to upload stuff to, pretty much like imgur does, but only through the API. Every user account has a token that the user can use to upload stuff through the API. You can find this token on the section called `Change your token` on the administration dashboard, and if it gets leaked or compromised you can renew it by clicking the button titled `Request new token`.
---
## Using loli-safe
Once the service starts you can start hitting the upload endpoint at `/api/upload` with any file. If you're using the frontend to do so then you are pretty much set, but if using the API to upload make sure the form name is set to `files[]` and the form type to `multipart/form-data`. If the service is running in private mode, dont forget to send a header of type `auth: YOUR-CLIENT-TOKEN` to validate the request.
Once the service starts you can start hitting the upload endpoint at `/api/upload` with any file. If you're using the frontend to do so then you are pretty much set, but if using the API to upload make sure the form name is set to `files[]` and the form type to `multipart/form-data`. If the service is running in private mode, dont forget to send a header of type `token: YOUR-CLIENT-TOKEN` to validate the request.
A sample of the returning json from the endpoint can be seen below:
```json
@ -38,3 +43,10 @@ Because of how nodejs apps work, if you want it attached to a domain name you wi
If you choose to use a domain name and thus nginx, you should add the following directive into your location block with the limit you want to set on uploaded file's size:
`client_max_body_size 512M;`
## Author
**loli-safe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/loli-safe/blob/master/LICENSE) License.<br>
Authored and maintained by Pitu.
> [lolisafe.moe](https://lolisafe.moe) · GitHub [@Pitu](https://github.com/Pitu) · Twitter [@kanaadeko](https://twitter.com/kanaadeko)

View File

@ -9,6 +9,9 @@ module.exports = {
*/
private: true,
// If true, users will be able to create accounts and access their uploaded files
enableUserAccounts: true,
// The registered domain where you will be serving the app. Use IP if none.
domains: [

View File

@ -5,93 +5,116 @@ let albumsController = {}
albumsController.list = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let fields = ['id', 'name']
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
if(req.params.sidebar === undefined)
fields.push('timestamp')
db.table('albums').select(fields).where('enabled', 1).then((albums) => {
let fields = ['id', 'name']
if(req.params.sidebar === undefined)
fields.push('timestamp')
if(req.params.sidebar !== undefined)
return res.json({ success: true, albums })
let ids = []
for(let album of albums){
album.date = new Date(album.timestamp * 1000)
album.date = album.date.getFullYear() + '-' + (album.date.getMonth() + 1) + '-' + album.date.getDate() + ' ' + (album.date.getHours() < 10 ? '0' : '') + album.date.getHours() + ':' + (album.date.getMinutes() < 10 ? '0' : '') + album.date.getMinutes() + ':' + (album.date.getSeconds() < 10 ? '0' : '') + album.date.getSeconds()
ids.push(album.id)
}
db.table('files').whereIn('albumid', ids).select('albumid').then((files) => {
let albumsCount = {}
db.table('albums').select(fields).where({enabled: 1, userid: user[0].id}).then((albums) => {
for(let id of ids) albumsCount[id] = 0
for(let file of files) albumsCount[file.albumid] += 1
for(let album of albums) album.files = albumsCount[album.id]
if(req.params.sidebar !== undefined)
return res.json({ success: true, albums })
return res.json({ success: true, albums })
let ids = []
for(let album of albums){
album.date = new Date(album.timestamp * 1000)
album.date = album.date.getFullYear() + '-' + (album.date.getMonth() + 1) + '-' + album.date.getDate() + ' ' + (album.date.getHours() < 10 ? '0' : '') + album.date.getHours() + ':' + (album.date.getMinutes() < 10 ? '0' : '') + album.date.getMinutes() + ':' + (album.date.getSeconds() < 10 ? '0' : '') + album.date.getSeconds()
ids.push(album.id)
}
db.table('files').whereIn('albumid', ids).select('albumid').then((files) => {
let albumsCount = {}
for(let id of ids) albumsCount[id] = 0
for(let file of files) albumsCount[file.albumid] += 1
for(let album of albums) album.files = albumsCount[album.id]
return res.json({ success: true, albums })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
albumsController.create = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let name = req.body.name
if(name === undefined || name === '')
return res.json({ success: false, description: 'No album name specified' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
db.table('albums').where('name', name).where('enabled', 1).then((album) => {
if(album.length !== 0) return res.json({ success: false, description: 'There\'s already an album with that name' })
let name = req.body.name
if(name === undefined || name === '')
return res.json({ success: false, description: 'No album name specified' })
db.table('albums').insert({
name: name,
db.table('albums').where({
name: name,
enabled: 1,
timestamp: Math.floor(Date.now() / 1000)
}).then(() => {
return res.json({ success: true })
})
userid: user[0].id
}).then((album) => {
if(album.length !== 0) return res.json({ success: false, description: 'There\'s already an album with that name' })
db.table('albums').insert({
name: name,
enabled: 1,
userid: user[0].id,
timestamp: Math.floor(Date.now() / 1000)
}).then(() => {
return res.json({ success: true })
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
albumsController.delete = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let id = req.body.id
if(id === undefined || id === '')
return res.json({ success: false, description: 'No album specified' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
db.table('albums').where('id', id).update({ enabled: 0 }).then(() => {
return res.json({ success: true })
let id = req.body.id
if(id === undefined || id === '')
return res.json({ success: false, description: 'No album specified' })
db.table('albums').where({id: id, userid: user[0].id}).update({ enabled: 0 }).then(() => {
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
albumsController.rename = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let id = req.body.id
if(id === undefined || id === '')
return res.json({ success: false, description: 'No album specified' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
let name = req.body.name
if(name === undefined || name === '')
return res.json({ success: false, description: 'No name specified' })
let id = req.body.id
if(id === undefined || id === '')
return res.json({ success: false, description: 'No album specified' })
db.table('albums').where('name', name).then((results) => {
if(results.length !== 0)
return res.json({ success: false, description: 'Name already in use' })
let name = req.body.name
if(name === undefined || name === '')
return res.json({ success: false, description: 'No name specified' })
db.table('albums').where('id', id).update({ name: name }).then(() => {
return res.json({ success: true })
db.table('albums').where({name: name, userid: user[0].id}).then((results) => {
if(results.length !== 0) return res.json({ success: false, description: 'Name already in use' })
db.table('albums').where({id: id, userid: user[0].id}).update({ name: name }).then(() => {
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })

View File

@ -0,0 +1,89 @@
const config = require('../config.js')
const db = require('knex')(config.database)
const bcrypt = require('bcrypt')
const saltRounds = 10
const randomstring = require('randomstring')
let authController = {}
authController.verify = function(req, res, next){
let username = req.body.username
let password = req.body.password
if(username === undefined) return res.json({ success: false, description: 'No username provided' })
if(password === undefined) return res.json({ success: false, description: 'No password provided' })
db.table('users').where('username', username).then((user) => {
if(user.length === 0) return res.json({ success: false, description: 'Username doesn\'t exist' })
bcrypt.compare(password, user[0].password, function(err, result) {
if(result === false) return res.json({ success: false, description: 'Wrong password' })
return res.json({ success: true, token: user[0].token })
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
authController.register = function(req, res, next){
if(config.enableUserAccounts === false)
return res.json({ success: false, description: 'Register is disabled at the moment' })
let username = req.body.username
let password = req.body.password
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(username.length < 4 || username.length > 32)
return res.json({ success: false, description: 'Username must have 6-32 characters' })
if(password.length < 6 || password.length > 64)
return res.json({ success: false, description: 'Password must have 6-64 characters' })
db.table('users').where('username', username).then((user) => {
if(user.length !== 0) return res.json({ success: false, description: 'Username already exists' })
bcrypt.hash(password, saltRounds, function(err, hash) {
if(err) return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
let token = randomstring.generate(64)
db.table('users').insert({
username: username,
password: hash,
token: token
}).then(() => {
return res.json({ success: true, token: token})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
authController.changePassword = function(req, res, next){
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
let password = req.body.password
if(password === undefined) return res.json({ success: false, description: 'No password provided' })
if(password.length < 6 || password.length > 64)
return res.json({ success: false, description: 'Password must have 6-64 characters' })
bcrypt.hash(password, saltRounds, function(err, hash) {
if(err) return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' })
db.table('users').where('id', user[0].id).update({password: hash}).then(() => {
return res.json({ success: true})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
module.exports = authController

View File

@ -1,60 +1,47 @@
const config = require('../config.js')
const db = require('knex')(config.database)
const randomstring = require('randomstring')
let tokenController = {}
tokenController.verify = function(req, res, next){
let type = req.body.type
if(req.body.token === undefined) return res.json({ success: false, description: 'No token provided' })
let token = req.body.token
if(type === undefined) return res.json({ success: false, description: 'No type provided.' })
if(token === undefined) return res.json({ success: false, description: 'No token provided.' })
if(type !== 'client' && type !== 'admin') return res.json({ success: false, description: 'Wrong type provided.' })
if(type === 'client'){
if(token !== config.clientToken) return res.json({ success: false, description: 'Token mismatch.' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.json({ success: false, description: 'Token mismatch' })
return res.json({ success: true })
}
if(type === 'admin'){
if(token !== config.adminToken) return res.json({ success: false, description: 'Token mismatch.' })
return res.json({ success: true })
}
return res.json({ success: false, description: '(╯°□°)╯︵ ┻━┻' })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
tokenController.list = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
return res.json({
clientToken: config.clientToken,
adminToken: config.adminToken
})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.json({ success: false, description: 'Token mismatch' })
return res.json({ success: true, token: token })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
tokenController.change = function(req, res, next){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let type = req.body.type
let token = req.body.token
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
if(type === undefined) return res.json({ success: false, description: 'No type provided.' })
if(token === undefined) return res.json({ success: false, description: 'No token provided.' })
if(type !== 'client' && type !== 'admin') return res.json({ success: false, description: 'Wrong type provided.' })
db.table('tokens').where('name', type).update({ value: token, timestamp: Math.floor(Date.now() / 1000) })
.then(() => {
if(type === 'client')
config.clientToken = token
else if(type === 'admin')
config.adminToken = token
res.json({ success: true })
let newtoken = randomstring.generate(64)
db.table('users').where('token', token).update({
token: newtoken,
timestamp: Math.floor(Date.now() / 1000)
}).then(() => {
res.json({ success: true, token: newtoken })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
module.exports = tokenController

View File

@ -25,81 +25,92 @@ const upload = multer({
uploadsController.upload = function(req, res, next){
if(config.private === true)
if(req.headers.auth !== config.clientToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
// Get the token
let token = req.headers.token
let album = req.params.albumid
// If we're running in private and there's no token, error
if(config.private === true)
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
// If there is no token then just leave it blank so the query fails
if(token === undefined) token = ''
if(album !== undefined)
if(req.headers.adminauth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
upload(req, res, function (err) {
if (err) {
console.error(err)
return res.json({
success: false,
description: err
})
db.table('users').where('token', token).then((user) => {
let userid
if(user.length > 0)
userid = user[0].id
// Check if user is trying to upload to an album
let album = undefined
if(userid !== undefined){
album = req.headers.albumid
if(album === undefined)
album = req.params.albumid
}
if(req.files.length === 0) return res.json({ success: false, description: 'no-files' })
upload(req, res, function (err) {
if (err) {
console.error(err)
return res.json({
success: false,
description: err
})
}
let files = []
let existingFiles = []
let iteration = 1
if(req.files.length === 0) return res.json({ success: false, description: 'no-files' })
req.files.forEach(function(file) {
let files = []
let existingFiles = []
let iteration = 1
// Check if the file exists by checking hash and size
let hash = crypto.createHash('md5')
let stream = fs.createReadStream('./' + config.uploads.folder + '/' + file.filename)
req.files.forEach(function(file) {
stream.on('data', function (data) {
hash.update(data, 'utf8')
})
// Check if the file exists by checking hash and size
let hash = crypto.createHash('md5')
let stream = fs.createReadStream('./' + config.uploads.folder + '/' + file.filename)
stream.on('end', function () {
let fileHash = hash.digest('hex') // 34f7a3113803f8ed3b8fd7ce5656ebec
db.table('files').where({
hash: fileHash,
size: file.size
}).then((dbfile) => {
if(dbfile.length !== 0){
uploadsController.deleteFile(file.filename).then(() => {}).catch((e) => console.error(e))
existingFiles.push(dbfile[0])
}else{
files.push({
name: file.filename,
original: file.originalname,
type: file.mimetype,
size: file.size,
hash: fileHash,
ip: req.ip,
albumid: album,
timestamp: Math.floor(Date.now() / 1000)
})
}
if(iteration === req.files.length)
return uploadsController.processFilesForDisplay(req, res, files, existingFiles)
iteration++
stream.on('data', function (data) {
hash.update(data, 'utf8')
})
stream.on('end', function () {
let fileHash = hash.digest('hex') // 34f7a3113803f8ed3b8fd7ce5656ebec
db.table('files').where({
hash: fileHash,
size: file.size
}).then((dbfile) => {
if(dbfile.length !== 0){
uploadsController.deleteFile(file.filename).then(() => {}).catch((e) => console.error(e))
existingFiles.push(dbfile[0])
}else{
files.push({
name: file.filename,
original: file.originalname,
type: file.mimetype,
size: file.size,
hash: fileHash,
ip: req.ip,
albumid: album,
userid: userid,
timestamp: Math.floor(Date.now() / 1000)
})
}
if(iteration === req.files.length)
return uploadsController.processFilesForDisplay(req, res, files, existingFiles)
iteration++
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
})
})
})
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
uploadsController.processFilesForDisplay = function(req, res, files, existingFiles){
let basedomain = req.get('host')
for(let domain of config.domains)
if(domain.host === req.get('host'))
@ -139,28 +150,38 @@ uploadsController.processFilesForDisplay = function(req, res, files, existingFil
uploadsController.delete = function(req, res){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let id = req.body.id
if(id === undefined || id === '')
return res.json({ success: false, description: 'No file specified' })
db.table('files').where('id', id).then((file) => {
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
uploadsController.deleteFile(file[0].name).then(() => {
db.table('files').where('id', id).del().then(() =>{
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch((e) => {
console.log(e.toString())
db.table('files').where('id', id).del().then(() =>{
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
db.table('files')
.where('id', id)
.where(function(){
if(user[0].username !== 'root')
this.where('userid', user[0].id)
})
.then((file) => {
uploadsController.deleteFile(file[0].name).then(() => {
db.table('files').where('id', id).del().then(() =>{
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch((e) => {
console.log(e.toString())
db.table('files').where('id', id).del().then(() =>{
return res.json({ success: true })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}
uploadsController.deleteFile = function(file){
@ -179,86 +200,92 @@ uploadsController.deleteFile = function(file){
uploadsController.list = function(req, res){
if(req.headers.auth !== config.adminToken)
return res.status(401).json({ success: false, description: 'not-authorized'})
let token = req.headers.token
if(token === undefined) return res.status(401).json({ success: false, description: 'No token provided' })
let offset = req.params.page
if(offset === undefined) offset = 0
db.table('users').where('token', token).then((user) => {
if(user.length === 0) return res.status(401).json({ success: false, description: 'Invalid token'})
db.table('files')
.where(function(){
if(req.params.id === undefined)
this.where('id', '<>', '')
else
this.where('albumid', req.params.id)
})
.orderBy('id', 'DESC')
.limit(25)
.offset(25 * offset)
.then((files) => {
db.table('albums').then((albums) => {
let offset = req.params.page
if(offset === undefined) offset = 0
let basedomain = req.get('host')
for(let domain of config.domains)
if(domain.host === req.get('host'))
if(domain.hasOwnProperty('resolve'))
basedomain = domain.resolve
db.table('files')
.where(function(){
if(req.params.id === undefined)
this.where('id', '<>', '')
else
this.where('albumid', req.params.id)
})
.where(function(){
if(user[0].username !== 'root')
this.where('userid', user[0].id)
})
.orderBy('id', 'DESC')
.limit(25)
.offset(25 * offset)
.then((files) => {
db.table('albums').then((albums) => {
for(let file of files){
file.file = basedomain + '/' + file.name
file.date = new Date(file.timestamp * 1000)
file.date = file.date.getFullYear() + '-' + (file.date.getMonth() + 1) + '-' + file.date.getDate() + ' ' + (file.date.getHours() < 10 ? '0' : '') + file.date.getHours() + ':' + (file.date.getMinutes() < 10 ? '0' : '') + file.date.getMinutes() + ':' + (file.date.getSeconds() < 10 ? '0' : '') + file.date.getSeconds()
let basedomain = req.get('host')
for(let domain of config.domains)
if(domain.host === req.get('host'))
if(domain.hasOwnProperty('resolve'))
basedomain = domain.resolve
file.album = ''
if(file.albumid !== undefined)
for(let album of albums)
if(file.albumid === album.id)
file.album = album.name
for(let file of files){
file.file = basedomain + '/' + file.name
file.date = new Date(file.timestamp * 1000)
file.date = file.date.getFullYear() + '-' + (file.date.getMonth() + 1) + '-' + file.date.getDate() + ' ' + (file.date.getHours() < 10 ? '0' : '') + file.date.getHours() + ':' + (file.date.getMinutes() < 10 ? '0' : '') + file.date.getMinutes() + ':' + (file.date.getSeconds() < 10 ? '0' : '') + file.date.getSeconds()
if(config.uploads.generateThumbnails === true){
file.album = ''
if(file.albumid !== undefined)
for(let album of albums)
if(file.albumid === album.id)
file.album = album.name
let extensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png']
for(let ext of extensions){
if(path.extname(file.name) === ext){
if(config.uploads.generateThumbnails === true){
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -4) + '.png'
let extensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png']
for(let ext of extensions){
if(path.extname(file.name) === ext){
let thumbname = path.join(__dirname, '..', 'uploads', 'thumbs') + '/' + file.name.slice(0, -4) + '.png'
fs.access(thumbname, function(err) {
if (err && err.code === 'ENOENT') {
// File doesnt exist
file.thumb = basedomain + '/thumbs/' + file.name.slice(0, -4) + '.png'
let size = {
width: 200,
height: 200
let thumbname = path.join(__dirname, '..', 'uploads', 'thumbs') + '/' + file.name.slice(0, -4) + '.png'
fs.access(thumbname, function(err) {
if (err && err.code === 'ENOENT') {
// File doesnt exist
let size = {
width: 200,
height: 200
}
gm('./' + config.uploads.folder + '/' + file.name)
.resize(size.width, size.height + '>')
.gravity('Center')
.extent(size.width, size.height)
.background('transparent')
.write(thumbname, function (error) {
if (error) console.log('Error - ', error)
})
}
gm('./' + config.uploads.folder + '/' + file.name)
.resize(size.width, size.height + '>')
.gravity('Center')
.extent(size.width, size.height)
.background('transparent')
.write(thumbname, function (error) {
if (error) console.log('Error - ', error)
})
}
})
})
}
}
}
}
}
return res.json({
success: true,
files
})
return res.json({
success: true,
files
})
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
}).catch(function(error) { console.log(error); res.json({success: false, description: 'error'}) })
})
}
module.exports = uploadsController

View File

@ -1,9 +1,9 @@
let init = function(db, config){
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.integer('enabled')
table.integer('timestamp')
@ -11,6 +11,7 @@ let init = function(db, config){
db.schema.createTableIfNotExists('files', function (table) {
table.increments()
table.integer('userid')
table.string('name')
table.string('original')
table.string('type')
@ -21,48 +22,28 @@ let init = function(db, config){
table.integer('timestamp')
}).then(() => {})
db.schema.createTableIfNotExists('tokens', function (table) {
table.string('name')
table.string('value')
db.schema.createTableIfNotExists('users', function (table) {
table.increments()
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
// == Generate a 1 time token == //
db.table('tokens').then((tokens) => {
if(tokens.length !== 0) return printAndSave(config, tokens[0].value, tokens[1].value)
// This is the first launch of the app
let clientToken = require('randomstring').generate()
let adminToken = require('randomstring').generate()
let now = Math.floor(Date.now() / 1000)
db.table('tokens').insert(
[
{
name: 'client',
value: clientToken,
timestamp: now
},
{
name: 'admin',
value: adminToken,
timestamp: now
}
]
).then(() => {
printAndSave(config, clientToken, adminToken)
}).catch(function(error) { console.log(error) })
}).catch(function(error) { console.log(error) })
require('bcrypt').hash('root', 10, function(err, hash) {
if(err) console.error('Error generating password hash for root')
db.table('users').insert({
username: 'root',
password: hash,
token: require('randomstring').generate(64),
timestamp: Math.floor(Date.now() / 1000)
}).then(() => {})
})
})
})
}
function printAndSave(config, clientToken, adminToken){
console.log('Your client token is: ' + clientToken)
console.log('Your admin token is: ' + adminToken)
config.clientToken = clientToken
config.adminToken = adminToken
}
module.exports = init

View File

@ -6,7 +6,7 @@ const db = require('knex')(config.database)
const fs = require('fs')
const safe = express()
require('./database/db.js')(db, config)
require('./database/db.js')(db)
fs.existsSync('./' + config.logsFolder) || fs.mkdirSync('./' + config.logsFolder)
fs.existsSync('./' + config.uploads.folder) || fs.mkdirSync('./' + config.uploads.folder)
@ -22,6 +22,7 @@ safe.use('/', express.static('./public'))
safe.use('/api', api)
safe.get('/', (req, res, next) => res.sendFile('home.html', { root: './pages/' }))
safe.get('/auth', (req, res, next) => res.sendFile('auth.html', { root: './pages/' }))
safe.get('/panel', (req, res, next) => res.sendFile('panel.html', { root: './pages/' }))
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/' }))

View File

@ -1,6 +1,6 @@
{
"name": "loli-safe",
"version": "1.0.0",
"version": "2.0.0",
"description": "Pomf-like uploading service, written in node",
"author": "kanadeko",
"repository": {
@ -15,6 +15,7 @@
},
"license": "MIT",
"dependencies": {
"bcrypt": "^1.0.2",
"body-parser": "^1.16.0",
"express": "^4.14.0",
"gm": "^1.23.0",

57
pages/auth.html Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>loli-safe - A self hosted upload service</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.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="/js/auth.js"></script>
</head>
<body>
<style type="text/css">
section#login {
background-color: #f5f6f8;
}
</style>
<section id='login' class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<h1 class="title">
Dashboard Access
</h1>
<h2 class="subtitle">
Login or register
</h2>
<div class="columns">
<div class="column">
<p class="control">
<input id='user' class="input" type="text" placeholder="Your username">
</p>
<p class="control">
<input id='pass' class="input" type="password" placeholder="Your password">
</p>
<p class="control has-addons is-pulled-right">
<a class="button" id='registerBtn' onclick="page.do('register')">
<span>Register</span>
</a>
<a class="button" id='loginBtn' onclick="page.do('login')">
<span>Log in</span>
</a>
</p>
</div>
<div class="column is-hidden-mobile"></div>
<div class="column is-hidden-mobile"></div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>loli-safe - A self hosted upload service</title>
<title>loli-safe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
@ -27,7 +27,7 @@
<div class="column" id='uploadContainer'>
<p id='tokenContainer' class="control has-addons has-addons-centered">
<input id='token' class="input is-danger" type="text" placeholder="Your upload token">
<input id='token' class="input is-danger" type="text" placeholder="Your token">
<a id='tokenSubmit' class="button is-danger">Check</a>
</p>
@ -48,7 +48,7 @@
</div>
<h3 id="links">
<a href="https://github.com/kanadeko/loli-safe" target="_blank" class="is-danger">View on Github</a><span>|</span><a href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj" target="_blank" class="is-danger">Chrome extension</a><span>|</span><a href="/panel" target="_blank" class="is-danger">Dashboard</a>
<a href="https://github.com/kanadeko/loli-safe" target="_blank" class="is-danger">View on Github</a><span>|</span><a href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj" target="_blank" class="is-danger">Chrome extension</a><span>|</span><a href="/auth" target="_blank" class="is-danger">Manage your uploads</a>
</h3>
</div>

View File

@ -53,7 +53,7 @@
</ul>
<p class="menu-label">Administration</p>
<ul class="menu-list">
<li><a id="itemTokens" onclick="panel.changeTokens()">Change your tokens</a></li>
<li><a id="itemTokens" onclick="panel.changeTokens()">Change your token</a></li>
<li><a onclick="panel.logout()">Logout</a></li>
</ul>
</aside>

View File

@ -78,6 +78,19 @@ section#home div#uploads { margin-bottom: 25px; }
PANEL
------------------ */
section#login input, section#login p.control a.button {
border-left: 0px;
border-top: 0px;
border-right: 0px;
border-radius: 0px;
box-shadow: 0 0 0;
}
section#login p.control a.button { margin-left: 10px; }
section#login p.control a#loginBtn { border-right: 0px; }
section#login p.control a#registerBtn { border-left: 0px; }
section#auth, section#dashboard { display: none }
section#auth input { background: rgba(0, 0, 0, 0); }
section#auth input, section#auth a {

56
public/js/auth.js Normal file
View File

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

View File

@ -1,17 +1,11 @@
let panel = {}
panel.page;
panel.token = localStorage.admintoken;
panel.token = localStorage.token;
panel.filesView = localStorage.filesView;
panel.preparePage = function(){
if(!panel.token){
document.getElementById('auth').style.display = 'flex';
document.getElementById('tokenSubmit').addEventListener('click', function(){
panel.verifyToken(document.getElementById('token').value);
});
return;
}
if(!panel.token) return window.location = '/auth';
panel.verifyToken(panel.token, true);
}
@ -20,7 +14,6 @@ panel.verifyToken = function(token, reloadOnError){
reloadOnError = false;
axios.post('/api/tokens/verify', {
type: 'admin',
token: token
})
.then(function (response) {
@ -32,15 +25,15 @@ panel.verifyToken = function(token, reloadOnError){
type: "error"
}, function(){
if(reloadOnError){
localStorage.removeItem("admintoken");
location.reload();
localStorage.removeItem("token");
location.location = '/auth';
}
})
return;
}
axios.defaults.headers.common['auth'] = token;
localStorage.admintoken = token;
axios.defaults.headers.common['token'] = token;
localStorage.token = token;
panel.token = token;
return panel.prepareDashboard();
@ -73,7 +66,7 @@ panel.prepareDashboard = function(){
}
panel.logout = function(){
localStorage.removeItem("admintoken");
localStorage.removeItem("token");
location.reload('/');
}
@ -85,14 +78,12 @@ panel.getUploads = function(album = undefined, page = undefined){
if(album !== undefined)
url = '/api/album/' + album + '/' + page
axios.get(url)
.then(function (response) {
axios.get(url).then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
var prevPage = 0;
var nextPage = page + 1;
@ -125,9 +116,7 @@ panel.getUploads = function(album = undefined, page = undefined){
if(panel.filesView === 'thumbs'){
container.innerHTML = `
${pagination}
<hr>
${listType}
@ -135,10 +124,8 @@ panel.getUploads = function(album = undefined, page = undefined){
</div>
${pagination}
`;
panel.page.appendChild(container);
var table = document.getElementById('table');
@ -157,7 +144,6 @@ panel.getUploads = function(album = undefined, page = undefined){
}else{
container.innerHTML = `
${pagination}
<hr>
${listType}
@ -175,7 +161,6 @@ panel.getUploads = function(album = undefined, page = undefined){
</table>
<hr>
${pagination}
`;
panel.page.appendChild(container);
@ -201,11 +186,7 @@ panel.getUploads = function(album = undefined, page = undefined){
table.appendChild(tr);
}
}
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
@ -238,7 +219,7 @@ panel.deleteFile = function(id){
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -258,10 +239,9 @@ panel.deleteFile = function(id){
panel.getAlbums = function(){
axios.get('/api/albums')
.then(function (response) {
axios.get('/api/albums').then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -324,7 +304,6 @@ panel.getAlbums = function(){
panel.submitAlbum();
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
@ -357,7 +336,7 @@ panel.renameAlbum = function(id){
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else if(response.data.description === 'Name already in use') swal.showInputError("That name is already in use!");
else swal("An error ocurred", response.data.description, "error");
return;
@ -396,7 +375,7 @@ panel.deleteAlbum = function(id){
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -424,7 +403,7 @@ panel.submitAlbum = function(){
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -446,7 +425,7 @@ panel.getAlbumsSidebar = function(){
axios.get('/api/albums/sidebar')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -489,7 +468,7 @@ panel.changeTokens = function(){
axios.get('/api/tokens')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -497,35 +476,21 @@ panel.changeTokens = function(){
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Manage your tokens</h2>
<h2 class="subtitle">Manage your token</h2>
<label class="label">Client token:</label>
<label class="label">Your current token:</label>
<p class="control has-addons">
<input id="clientToken" class="input is-expanded" type="text" placeholder="Your client token">
<a id="submitClientToken" class="button is-primary">Save</a>
</p>
<label class="label">Admin token:</label>
<p class="control has-addons">
<input id="adminToken" class="input is-expanded" type="text" placeholder="Your admin token">
<a id="submitAdminToken" class="button is-primary">Save</a>
<input id="token" readonly class="input is-expanded" type="text" placeholder="Your token" value="${response.data.token}">
<a id="getNewToken" class="button is-primary">Request new token</a>
</p>
`;
panel.page.appendChild(container);
document.getElementById('clientToken').value = response.data.clientToken;
document.getElementById('adminToken').value = response.data.adminToken;
document.getElementById('submitClientToken').addEventListener('click', function(){
panel.submitToken('client', document.getElementById('clientToken').value);
document.getElementById('getNewToken').addEventListener('click', function(){
panel.getNewToken();
});
document.getElementById('submitAdminToken').addEventListener('click', function(){
panel.submitToken('admin', document.getElementById('adminToken').value);
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
@ -534,16 +499,13 @@ panel.changeTokens = function(){
}
panel.submitToken = function(type, token){
panel.getNewToken = function(){
axios.post('/api/tokens/change', {
type: type,
token: token
})
axios.post('/api/tokens/change')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'not-authorized') return panel.verifyToken(panel.token);
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
@ -552,14 +514,8 @@ panel.submitToken = function(type, token){
text: 'Your token was changed successfully.',
type: "success"
}, function(){
if(type === 'client')
localStorage.token = token;
else if(type === 'admin')
localStorage.admintoken = token
localStorage.token = response.data.token;
location.reload();
})
})

View File

@ -36,7 +36,6 @@ upload.verifyToken = function(token, reloadOnError){
reloadOnError = false;
axios.post('/api/tokens/verify', {
type: 'client',
token: token
})
.then(function (response) {
@ -101,10 +100,11 @@ upload.prepareDropzone = function(){
maxFiles: 1000,
autoProcessQueue: true,
headers: {
'auth': upload.token
'token': upload.token
},
init: function() {
this.on('addedfile', function(file) {
myDropzone = this;
document.getElementById('uploads').style.display = 'block';
});
}
@ -139,6 +139,22 @@ upload.prepareDropzone = function(){
}
//Handle image paste event
window.addEventListener('paste', function(event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (index in items) {
var item = items[index];
if (item.kind === 'file') {
var blob = item.getAsFile();
console.log(blob.type);
var file = new File([blob], "pasted-image."+blob.type.match(/(?:[^\/]*\/)([^;]*)/)[1]);
file.type = blob.type;
console.log(file);
myDropzone.addFile(file);
}
}
});
window.onload = function () {
upload.checkIfPublic();
};

View File

@ -3,6 +3,7 @@ const routes = require('express').Router()
const uploadController = require('../controllers/uploadController')
const albumsController = require('../controllers/albumsController')
const tokenController = require('../controllers/tokenController')
const authController = require('../controllers/authController')
routes.get ('/check', (req, res, next) => {
return res.json({
@ -11,6 +12,9 @@ routes.get ('/check', (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.get ('/uploads', (req, res, next) => uploadController.list(req, res))
routes.get ('/uploads/:page', (req, res, next) => uploadController.list(req, res))
routes.post ('/upload', (req, res, next) => uploadController.upload(req, res, next))