Merge branch 'dev'

This commit is contained in:
Pitu 2021-01-04 01:04:20 +09:00
commit fcd39dc550
210 changed files with 30519 additions and 5967 deletions

46
.dockerignore Normal file
View File

@ -0,0 +1,46 @@
# Packages
node_modules
**/node_modules
# Log files
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# IDE
.vscode
# Docker (experimental)
docker/
Dockerfile
.dockerignore
docker-compose.yml
docker-compose.config.yml
docker-compose.config.example.yml
# Tests
coverage/
jest-setup.ts
jest.config.js
# Linting
.eslingignore
.eslintrc.json
tsconfig.eslint.json
# Miscellaneous
.tmp
.vscode
.git
.gitattributes
.gitignore
README.md
chibi.ps1
chibi.sh
dist
.nuxt

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

25
.gitignore vendored
View File

@ -1,14 +1,15 @@
.DS_Store
!.gitkeep
# Packages
node_modules/
uploads/
dist/
.nuxt/
logs/
database/db
config.js
start.json
npm-debug.log
pages/custom/**
migrate.js
yarn.lock
package-lock.json
.vscode/
# Chibisafe specifics
database.sqlite
uploads/
.env
!src/api/routes/uploads
db
database.sqlite-journal
docker/nginx/chibisafe.moe.conf
docker-compose.config.yml
/coverage

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
audit=false
fund=false
node-version=false
legacy-peer-deps=true

50
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,50 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "[DEV] Launch API",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\src\\api\\structures\\Server"
},
{
"type": "node",
"request": "launch",
"name": "[DEV] Launch ThumbGen",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\src\\api\\utils\\generateThumbs"
},
{
"type": "chrome",
"request": "launch",
"name": "client: chrome",
"url": "http://localhost:8070",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "server: nuxt",
"args": ["dev"],
"osx": {
"program": "${workspaceFolder}/node_modules/.bin/nuxt"
},
"linux": {
"program": "${workspaceFolder}/node_modules/.bin/nuxt"
},
"windows": {
"program": "${workspaceFolder}/node_modules/nuxt/bin/nuxt.js"
}
}
],
"compounds": [
{
"name": "fullstack: nuxt",
"configurations": ["server: nuxt", "client: chrome"]
}
]
}

22
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"files.insertFinalNewline": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[vue]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"prettier.disableLanguages": ["vue"],
"vetur.format.enable": true,
"files.eol": "\n",
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}

View File

@ -1,19 +0,0 @@
FROM node:9
LABEL name "lolisafe"
LABEL version "3.0.0"
LABEL maintainer "iCrawl <icrawltogo@gmail.com>"
WORKDIR /usr/src/lolisafe
COPY package.json yarn.lock ./
RUN sh -c 'echo "deb http://www.deb-multimedia.org jessie main" >> /etc/apt/sources.list' \
&& apt-key adv --keyserver keyring.debian.org --recv-keys 5C808C2B65558117 \
&& apt-get update \
&& apt-get install -y ffmpeg graphicsmagick \
&& yarn install
COPY . .
CMD ["node", "lolisafe.js"]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 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,71 +1,54 @@
![lolisafe](https://lolisafe.moe/8KFePddY.png)
<p align="center">
<img width="234" height="376" src="https://lolisafe.moe/xjoghu.png">
</p>
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/kanadeko/Kuro/master/LICENSE)
[![Chat / Support](https://img.shields.io/badge/Chat%20%2F%20Support-discord-7289DA.svg?style=flat-square)](https://discord.gg/5g6vgwn)
[![Support me](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fpitu&style=flat-square)](https://www.patreon.com/pitu)
[![Support me](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dpitu%26type%3Dpledges&style=flat-square)](https://www.patreon.com/pitu)
[![Support me](https://img.shields.io/badge/Support-Buy%20me%20a%20coffee-yellow.svg?style=flat-square)](https://www.buymeacoffee.com/kana)
# lolisafe, a small safe worth protecting.
### Attention
If you are upgrading from v3 to v4 (current release) and you want to keep your files and relations please read the [migration guide](docs/migrating.md).
## What's new in v3.0.0
- Backend rewrite to make it faster, better and easier to extend
- Album downloads (Thanks to [PascalTemel](https://github.com/PascalTemel))
- See releases for changelog
### What is this?
Chibisafe is a file uploader service written in node that aims to to be easy to use and easy to set up. It's mainly intended for images and videos, but it accepts anything you throw at it.
- You can run it in public or private mode, making it so only people with user accounts can upload files as well as controlling if user signup is enabled or not.
- Out of the box support for ShareX configuration letting you upload screenshots and screencaptures directly to your chibisafe instance.
- Browser extension to be able to right click any image/video and upload it directly to your chibisafe instance.
- Chunk uploads enabled by default to be able to handle big boi files.
- API Key support so you can integrate the service with whatever you desire.
- Albums, tags and Discord-like search function
- User list and control panel
If you're upgrading from a version prior to v3.0.0 make sure to run `node database/migration.js` **ONCE** to create the missing columns on the database.
### Pre-requisites
This guide asumes a lot of things, including that you know your way around linux, nginx and internet in general.
## Running
1. Ensure you have at least version 7.6.0 of node installed
2. Clone the repo
3. Rename `config.sample.js` to `config.js`
4. Modify port, domain and privacy options if desired
5. run `npm install` to install all dependencies
6. run `pm2 start lolisafe.js` or `node lolisafe.js` to start the service
- Decently updated version of linux
- `node` version 12+
- `build-essential` package installed to build dependencies
- `ffmpeg` package installed if you want video thumbnails
- `yarn` package installed. If you'd like to use npm instead change `package.json` accordingly
- `pm2` globally installed (`npm i -g pm2`) to keep the service alive at all times.
- A database, postgresql preferably. You can also fall back to sqlite3 which ships by default.
## 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`.
### Installing
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.
1. Clone the repository and `cd` into it
2. Run `yarn install`
3. Run `yarn setup`
The option `serveFilesWithNode` in the `config.js` dictates if you want lolisafe to serve the files or nginx/apache once they are uploaded. 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:
https://lolisafe.moe/yourFile.jpg
Chibisafe is now installed, configured and ready. Now you need to serve it to the public by using a domain name.
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:
https://files.lolisafe.moe/yourFile.jpg
6. Check the [nginx](docs/nginx.md) file for a sample configuration that has every step to run chibisafe securely on production.
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. Either way, I've provided a sample config files for nginx that you can use to set it up quickly and painlessly!
- [Normal Version](https://github.com/WeebDev/lolisafe/blob/master/nginx.sample.conf)
- [SSL Version](https://github.com/WeebDev/lolisafe/blob/master/nginx-ssl.sample.conf)
After you finish setting up nginx, you need to start chibisafe by using pm2. If you want to use something else like forever, ensure that the process spawned from `npm run start` never dies.
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`.
## Cloudflare Support
If you are running lolisafe behind Cloudflare there is support to make the NGINX logs have the user's IP instead of Cloudflare's IP. You will need to compile NGINX from source with `--with-http_realip_module` as well as uncomment the following line in the NGINX config: `include /path/to/lolisafe/real-ip-from-cf;`
## Using lolisafe
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 you're 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, don't forget to send a header of type `token: YOUR-CLIENT-TOKEN` to validate the request.
A sample of the JSON returned from the endpoint can be seen below:
```json
{
"name": "EW7C.png",
"size": "71400",
"url": "https://i.kanacchi.moe/EW7C.png"
}
```
To make it easier and better than any other service, you can download [our Chrome extension](https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj). That will let you configure your hostname and tokens, so that you can simply `right click` -> `loli-safe` -> `send to safe` on any image/audio/video file on the web.
Because of how nodejs apps work, if you want it attached to a domain name you will need to make a reverse proxy for it. Here is a tutorial [on how to do this with nginx](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04). Keep in mind that this is only a requirement if you want to access your lolisafe service by using a domain name, otherwise you can use the service just fine by accessing it from your server's IP.
## Sites using lolisafe
Refer to the [wiki](https://github.com/WeebDev/lolisafe/wiki/Sites-using-lolisafe)
7. Run `pm2 start pm2.json`:
8. Profit
## Author
**lolisafe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/lolisafe/blob/master/LICENSE) License.<br>
**Chibisafe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/chibisafe/blob/master/LICENSE) License.<br>
Authored and maintained by Pitu.
> [lolisafe.moe](https://lolisafe.moe) · GitHub [@Pitu](https://github.com/Pitu)
> [chibisafe.moe](https://chibisafe.moe) · GitHub [@Pitu](https://github.com/Pitu)

13
TODO Normal file
View File

@ -0,0 +1,13 @@
- When you are on the table view of files, the column `Albums` doesn't get populated unless you fetch them, obviously.
- Think of a strategy to achieve this in a nice manner
- There's nowhere to rename albums
- Finish /dashboard/tags - right now all it does is ask if you want to delete a tag (make it like the albums page)
- Make the settings page work properly
- Add an stats page that displays statistics about the server like free space, free/used memory, etc
- Add statistics of total disk space used by a specific user (?)
- Page that lists all files ordered by size, useful to find big files
- databaseMigration.js generates thumbnails, make it use the ThumbUtil.js functions for consistency
- Add pagination to user files
- Add pagination to public album links

21
babel.config.js Normal file
View File

@ -0,0 +1,21 @@
function isBabelLoader(caller) {
return caller && caller.name === 'babel-loader';
}
module.exports = function(api) {
if (api.env('test') && !api.caller(isBabelLoader)) {
return {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
]
]
};
}
return {};
};

View File

@ -1,103 +0,0 @@
module.exports = {
/*
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
unless they have the token lolisafe provides you with.
If it's set to false, then upload will be public for anyone to use.
*/
private: true,
// If true, users will be able to create accounts and access their uploaded files
enableUserAccounts: true,
/*
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.
If you set it to `true`, the uploaded files will be located after the host like:
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
downloads in. This also gives you the ability to serve them, for example, like this:
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.
Which one you use is ultimately up to you.
*/
serveFilesWithNode: false,
domain: 'https://lolisafe.moe',
// Port on which to run the server
port: 9999,
// Pages to process for the frontend
pages: ['home', 'auth', 'dashboard', 'faq'],
// Add file extensions here which should be blocked
blockedExtensions: [
'.jar',
'.exe',
'.exec',
'.msi',
'.com',
'.bat',
'.cmd',
'.nt',
'.scr',
'.ps1',
'.psm1',
'.sh',
'.bash',
'.bsh',
'.csh',
'.bash_profile',
'.bashrc',
'.profile'
],
// Uploads config
uploads: {
// Folder where images should be stored
folder: 'uploads',
/*
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.
*/
maxSize: '512MB',
// The length of the random generated name for the uploaded files
fileLength: 32,
/*
This option will limit how many times it will try to generate random names
for uploaded files. If this value is higher than 1, it will help in cases
where files with the same name already exists (higher chance with shorter file name length).
*/
maxTries: 1,
/*
NOTE: Thumbnails are only for the admin panel and they require you
to install a separate binary called ffmpeg (https://ffmpeg.org/) for video files
*/
generateThumbnails: false,
/*
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
and is re-used if the album has not changed between download requests
*/
generateZips: true
},
// Folder where to store logs
logsFolder: 'logs',
// The following values shouldn't be touched
database: {
client: 'sqlite3',
connection: { filename: './database/db' },
useNullAsDefault: true
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,53 +0,0 @@
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');
table.integer('editedAt');
table.integer('zipGeneratedAt');
}).then(() => {});
db.schema.createTableIfNotExists('files', function (table) {
table.increments();
table.integer('userid');
table.string('name');
table.string('original');
table.string('type');
table.string('size');
table.string('hash');
table.string('ip');
table.integer('albumid');
table.integer('timestamp');
}).then(() => {});
db.schema.createTableIfNotExists('users', function (table) {
table.increments();
table.string('username');
table.string('password');
table.string('token');
table.integer('enabled');
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) {
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(() => {});
});
});
});
};
module.exports = init;

View File

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

3
docker/chibi.ps1 Normal file
View File

@ -0,0 +1,3 @@
$env = $args[0]
$cmd = $args | Select-Object -Skip 1
docker-compose -f docker-compose.yml -f docker-compose.$env.yml -f docker-compose.config.yml $cmd

2
docker/chibi.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
docker-compose -f docker-compose.yml -f docker-compose.$1.yml -f docker-compose.config.yml ${@%$1}

View File

@ -0,0 +1,20 @@
FROM jrottenberg/ffmpeg:4.3-alpine312 as ffmpeg
FROM node:alpine3.12
WORKDIR /usr/chibisafe
COPY package.json package-lock.json ./
RUN apk add --update \
&& apk add --no-cache ca-certificates libwebp libwebp-tools expat \
&& apk add --no-cache vidstab-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \
&& apk add --no-cache --virtual .build-deps git curl build-base python3 g++ make \
&& npm ci \
&& apk del .build-deps
COPY --from=ffmpeg /usr/local /usr/local
COPY . .
RUN mkdir uploads && mkdir database
CMD ["sh", "-c", "npm run migrate && npm run seed && npm run build && npm start"]

View File

@ -0,0 +1,37 @@
version: "3.7"
services:
chibisafe:
environment:
CHUNK_SIZE: 90
SECRET: "wowfcgMHqZHwOIMLadWrKu3liyqPOOILpDLSDvuxq3YGhJmiZXJCVpnF96l11WfR"
ADMIN_ACCOUNT: "admin"
ADMIN_PASSWORD: "admin"
# ROUTE_PREFIX: /api
# RATE_LIMIT_WINDOW: 2
# RATE_LIMIT_MAX: 5
# BLOCKED_EXTENSIONS: '.jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh'
# UPLOAD_FOLDER: uploads
# MAX_LINKS_PER_ALBUM: 5
# META_THEME_COLOR: '#20222b'
# META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀'
# META_KEYWORDS: 'chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free'
# META_TWITTER_HANDLE: ''
# SERVER_PORT: 5000
# WEBSITE_PORT: 5001
# DOMAIN: 'http://chibisafe.moe'
# SERVICE_NAME: chibisafe
# MAX_SIZE: 5000
# GENERATE_THUMBNAILS: 'true'
# GENERATE_ZIPS: 'true'
# STRIP_EXIF: 'true'
# SERVE_WITH_NODE: 'true'
# GENERATED_FILENAME_LENGTH: 6
# GENERATED_ALBUM_LENGTH: 4
# PUBLIC_MODE: 'false'
# USER_ACCOUNTS: 'true'
# DB_CLIENT: 'sqlite3'
# DB_HOST: ''
# DB_USER: ''
# DB_PASSWORD: ''
# DB_DATABASE: ''

View File

@ -0,0 +1,19 @@
version: "3.7"
services:
nginx:
volumes:
- nginx-data:/etc/nginx
chibisafe:
volumes:
- chibisafe-data:/usr/chibisafe/uploads
- chibisafe-database:/usr/chibisafe/database
volumes:
nginx-data:
name: "nginx-data"
chibisafe-data:
name: "chibisafe-data"
chibisafe-database:
name: "chibisafe-database"

View File

@ -0,0 +1,11 @@
version: "3.7"
services:
nginx:
volumes:
- ./nginx-data:/etc/nginx
chibisafe:
volumes:
- ./chibisafe-data:/usr/chibisafe/uploads
- ./chibisafe-database:/usr/chibisafe/database

63
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,63 @@
version: "3.7"
services:
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
expose:
- "80"
- "443"
ports:
- "80:80"
- "443:443"
restart: unless-stopped
healthcheck:
test: ["CMD", "service", "nginx", "status"]
interval: 60s
timeout: 5s
chibisafe:
build:
context: ../
dockerfile: ./docker/chibisafe/Dockerfile
expose:
- "5000"
- "5001"
ports:
- "5000"
- "5001"
restart: unless-stopped
environment:
CHUNK_SIZE: 90
ROUTE_PREFIX: /api
RATE_LIMIT_WINDOW: 2
RATE_LIMIT_MAX: 5
BLOCKED_EXTENSIONS: ".jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh"
UPLOAD_FOLDER: uploads
SECRET: ""
MAX_LINKS_PER_ALBUM: 5
META_THEME_COLOR: "#20222b"
META_DESCRIPTION: "Blazing fast file uploader and bunker written in node! 🚀"
META_KEYWORDS: "chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free"
META_TWITTER_HANDLE: ""
SERVER_PORT: 5000
WEBSITE_PORT: 5001
DOMAIN: "http://chibisafe.moe"
SERVICE_NAME: chibisafe
MAX_SIZE: 5000
GENERATE_THUMBNAILS: "true"
GENERATE_ZIPS: "true"
STRIP_EXIF: "true"
SERVE_WITH_NODE: "true"
GENERATED_FILENAME_LENGTH: 6
GENERATED_ALBUM_LENGTH: 4
PUBLIC_MODE: "false"
USER_ACCOUNTS: "true"
ADMIN_ACCOUNT: ""
ADMIN_PASSWORD: ""
DB_CLIENT: "sqlite3"
DB_HOST: ""
DB_USER: ""
DB_PASSWORD: ""
DB_DATABASE: ""

5
docker/nginx/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM nginx
COPY nginxconfig.io /etc/nginx/nginxconfig.io
COPY nginx.conf /etc/nginx/nginx.conf
COPY chibisafe.moe.conf /etc/nginx/conf.d/chibisafe.moe.conf

View File

@ -0,0 +1,21 @@
server {
listen 80;
listen [::]:80;
server_name chibisafe.moe;
# security
include nginxconfig.io/security.conf;
# logging
access_log /var/log/nginx/chibisafe.moe.access.log;
error_log /var/log/nginx/chibisafe.moe.error.log warn;
# reverse proxy
location / {
proxy_pass http://localhost:5000;
include nginxconfig.io/proxy.conf;
}
# additional config
include nginxconfig.io/general.conf;
}

View File

@ -0,0 +1,32 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name chibisafe.moe;
# SSL
ssl_certificate /etc/nginx/ssl/chibisafe.moe.crt;
ssl_certificate_key /etc/nginx/ssl/chibisafe.moe.key;
# security
include nginxconfig.io/security.conf;
# logging
access_log /var/log/nginx/chibisafe.moe.access.log;
error_log /var/log/nginx/chibisafe.moe.error.log warn;
# reverse proxy
location / {
proxy_pass http://localhost:5000;
include nginxconfig.io/proxy.conf;
}
# additional config
include nginxconfig.io/general.conf;
}
# HTTP redirect
server {
listen 80;
listen [::]:80;
return 301 https://$server_name$request_uri;
}

54
docker/nginx/nginx.conf Normal file
View File

@ -0,0 +1,54 @@
# Generated by nginxconfig.io
# https://www.digitalocean.com/community/tools/nginx?domains.0.server.domain=tourneys.naval-base.com&domains.0.server.documentRoot=&domains.0.https.certType=custom&domains.0.php.php=false&domains.0.reverseProxy.reverseProxy=true&domains.0.reverseProxy.proxyPass=http%3A%2F%2F127.0.0.1%3A3001&domains.0.routing.root=false&domains.0.logging.accessLog=true&domains.0.logging.errorLog=true
user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;
events {
multi_accept on;
worker_connections 65535;
}
http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 90M;
client_body_timeout 600s;
# MIME
include mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites
# ssl_dhparam /etc/nginx/dhparam.pem;
# Mozilla Intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# OCSP Stapling
ssl_stapling off;
ssl_stapling_verify off;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;
# Load configs
include /etc/nginx/conf.d/*.conf;
# include /etc/nginx/sites-enabled/*;
}

View File

@ -0,0 +1,18 @@
# favicon.ico
location = /favicon.ico {
log_not_found off;
access_log off;
}
# robots.txt
location = /robots.txt {
log_not_found off;
access_log off;
}
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

View File

@ -0,0 +1,18 @@
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
# Proxy headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-NginX-Proxy true;
# Proxy timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

View File

@ -0,0 +1,12 @@
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# . files
location ~ /\.(?!well-known) {
deny all;
}

17
docs/migrating.md Normal file
View File

@ -0,0 +1,17 @@
### Migrate from v3 to v4
This version introduces a few breaking changes and updating requires some manual work.
For starters we recommend cloning the new version somewhere else instead of `git pull` on your v3 version.
- After cloning move your `uploads` folder from the v3 folder to the new v4 folder.
- Then copy your `database/db` file from your v3 folder to the root of your v4 folder.
- You then need to run `yarn setup` or `npm start setup` from the v4 folder and finish the setup process.
- Once that's done you need to manually run `node src/api/databaseMigration.js` from the root folder of v4.
- This will migrate the v3 database to v4 and regenerate every single thumbnail in webp to save bandwidth.
- After the migration finishes, the last step is to update your nginx config with the [newly provided script](./nginx.md).
- Restart nginx with `sudo nginx -s reload`.
- And lastly start your chibisafe instance with `pm2 start pm2.json`.
### Breaking changes
- If you are using the chibisafe extension from one of the stores, the new version has been submitted already. You can also load the unpacked extension by cloning [this repo](https://github.com/WeebDev/chibisafe-extension).
- The chibisafe browser extension needs your new token. Instead of pasting your jwt token into it like before, you need to log in to chibisafe, go to your user settings and generate an `API KEY`, which you will use to access the service from 3rd party apps like the browser extension, ShareX, etc.
- To upload a file to an album directly users used to use the endpoint `/api/upload/${albumId}`. This is no longer the case. To upload directly to an album now it's necessary to pass a header called `albumid` with an integer as the value of the album to which you want to upload the file to.

67
docs/nginx.md Normal file
View File

@ -0,0 +1,67 @@
### Nginx config for SSL
Make sure that:
- `backend` port matches your wizard config
- `client_max_body_size` matches your wizard config
- You replace `your.domain` where pertinent
```nginx
upstream backend {
server 127.0.0.1:5000;
}
server {
listen 80;
listen [::]:80;
server_name your.domain;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your.domain;
ssl_certificate /path/to/certificate.pem;
ssl_certificate_key /path/to/certificate.key;
ssl_trusted_certificate /path/to/certificate.pem;
access_log /var/log/nginx/your.domain.access.log;
error_log /var/log/nginx/your.domain.error.log;
# Security
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-
GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SH
A:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM
-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
add_header X-XSS-Protection "1; mode=block";
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
client_max_body_size 90M;
client_body_timeout 600s;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```

9
jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'vue'],
moduleDirectories: ['node_modules'],
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
transformIgnorePatterns: ['/node_modules/(?!vue)']
};

23
knexfile.js Normal file
View File

@ -0,0 +1,23 @@
require('dotenv').config();
module.exports = {
client: process.env.DB_CLIENT,
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
filename: 'database/database.sqlite'
},
pool: {
min: process.env.DATABASE_POOL_MIN || 2,
max: process.env.DATABASE_POOL_MAX || 10
},
migrations: {
directory: 'src/api/database/migrations'
},
seeds: {
directory: 'src/api/database/seeds'
},
useNullAsDefault: process.env.DB_CLIENT === 'sqlite3'
};

View File

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

View File

@ -1,56 +0,0 @@
upstream backend {
server 127.0.0.1:9999; # Change to the port you specified on lolisafe
}
map $sent_http_content_type $charset {
~^text/ utf-8;
}
server {
listen 80;
listen [::]:80;
server_name lolisafe.moe;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name lolisafe.moe;
server_tokens off;
ssl_certificate /path/to/your/fullchain.pem;
ssl_certificate_key /path/to/your/privkey.pem;
ssl_trusted_certificate /path/to/your/fullchain.pem;
client_max_body_size 100M; # Change this to the max file size you want to allow
charset $charset;
charset_types *;
# Uncomment if you are running lolisafe behind CloudFlare.
# This requires NGINX compiled from source with:
# --with-http_realip_module
#include /path/to/lolisafe/real-ip-from-cf;
location / {
add_header Access-Control-Allow-Origin *;
root /path/to/your/uploads/folder;
try_files $uri @proxy;
}
location @proxy {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -1,45 +0,0 @@
upstream backend {
server 127.0.0.1:9999; # Change to the port you specified on lolisafe
}
map $sent_http_content_type $charset {
~^text/ utf-8;
}
server {
listen 80;
listen [::]:80;
server_name lolisafe.moe;
server_tokens off;
client_max_body_size 100M; # Change this to the max file size you want to allow
charset $charset;
charset_types *;
# Uncomment if you are running lolisafe behind CloudFlare.
# This requires NGINX compiled from source with:
# --with-http_realip_module
#include /path/to/lolisafe/real-ip-from-cf;
location / {
add_header Access-Control-Allow-Origin *;
root /path/to/your/uploads/folder;
try_files $uri @proxy;
}
location @proxy {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

99
nuxt.config.js Normal file
View File

@ -0,0 +1,99 @@
import dotenv from 'dotenv/config';
import autoprefixer from 'autoprefixer';
import jetpack from 'fs-jetpack';
const clientConfig = {
development: process.env.NODE_ENV !== 'production',
version: process.env.npm_package_version,
URL: process.env.DOMAIN,
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`,
serviceName: process.env.SERVICE_NAME,
maxFileSize: parseInt(process.env.MAX_SIZE, 10),
chunkSize: parseInt(process.env.CHUNK_SIZE, 10),
maxLinksPerAlbum: parseInt(process.env.MAX_LINKS_PER_ALBUM, 10),
publicMode: process.env.PUBLIC_MODE === 'true',
userAccounts: process.env.USER_ACCOUNTS === 'true'
};
export default {
mode: 'spa',
server: {
port: process.env.WEBSITE_PORT
},
srcDir: 'src/site/',
head: {
title: process.env.SERVICE_NAME,
titleTemplate: `%s | ${process.env.SERVICE_NAME}`,
// TODO: Add the directory with pictures for favicon and stuff
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'theme-color', name: 'theme-color', content: `${process.env.META_THEME_COLOR}` },
{ hid: 'description', name: 'description', content: `${process.env.META_DESCRIPTION}` },
{ hid: 'keywords', name: 'keywords', content: `${process.env.META_KEYWORDS}` },
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: `${process.env.SERVICE_NAME}`
},
{ hid: 'application-name', name: 'application-name', content: `${process.env.SERVICE_NAME}` },
// { hid: 'msapplication-config', name: 'msapplication-config', content: `${process.env.DOMAIN}/browserconfig.xml` },
{ hid: 'twitter:card', name: 'twitter:card', content: 'summary_large_image' },
{ hid: 'twitter:site', name: 'twitter:site', content: `${process.env.META_TWITTER_HANDLE}` },
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${process.env.META_TWITTER_HANDLE}` },
{ hid: 'twitter:title', name: 'twitter:title', content: `${process.env.SERVICE_NAME}` },
{ hid: 'twitter:description', name: 'twitter:description', content: `${process.env.META_DESCRIPTION}` },
{ hid: 'twitter:image', name: 'twitter:image', content: `${process.env.DOMAIN}/share.jpg` },
{ hid: 'og:url', property: 'og:url', content: `${process.env.DOMAIN}` },
{ hid: 'og:type', property: 'og:type', content: 'website' },
{ hid: 'og:title', property: 'og:title', content: `${process.env.SERVICE_NAME}` },
{ hid: 'og:description', property: 'og:description', content: `${process.env.META_DESCRIPTION}` },
{ hid: 'og:image', property: 'og:image', content: `${process.env.DOMAIN}/share.jpg` },
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${process.env.DOMAIN}/share.jpg` },
{ hid: 'og:site_name', property: 'og:site_name', content: `${process.env.SERVICE_NAME}` }
],
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
// This one is a pain in the ass to make it customizable, so you should edit it manually
{ type: 'application/json+oembed', href: `${process.env.DOMAIN}/oembed.json` }
]
},
plugins: [
'~/plugins/axios',
'~/plugins/buefy',
'~/plugins/v-clipboard',
'~/plugins/vue-isyourpasswordsafe',
'~/plugins/vue-timeago',
'~/plugins/vuebar',
'~/plugins/nuxt-client-init',
'~/plugins/notifier',
'~/plugins/handler'
],
css: [],
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt'],
router: {
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-active'
},
axios: {
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`
},
build: {
extractCSS: process.env.NODE_ENV === 'production',
postcss: {
preset: {
autoprefixer
}
},
extend(config, { isClient, isDev }) {
// Extend only webpack config for client-bundle
if (isClient) {
jetpack.write('dist/config.json', clientConfig);
}
if (isDev) {
config.devtool = isClient ? 'source-map' : 'inline-source-map';
}
}
}
};

18492
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,141 @@
{
"name": "lolisafe",
"version": "3.0.0",
"description": "Blazing fast file uploader and awesome bunker written in node! 🚀",
"author": "Pitu",
"repository": {
"type": "git",
"url": "https://github.com/WeebDev/lolisafe"
},
"bugs": {
"url": "https://github.com/WeebDev/lolisafe/issues"
},
"engines": {
"node": ">=7.0.0"
},
"license": "MIT",
"dependencies": {
"bcrypt": "^3.0.4",
"body-parser": "^1.18.2",
"express": "^4.16.1",
"express-handlebars": "^3.0.0",
"express-rate-limit": "^2.11.0",
"fluent-ffmpeg": "^2.1.2",
"helmet": "^3.11.0",
"jszip": "^3.1.5",
"knex": "^0.14.4",
"multer": "^1.3.0",
"randomstring": "^1.1.5",
"sharp": "^0.21.0",
"sqlite3": "^4.0.6"
},
"devDependencies": {
"eslint": "^4.18.2",
"eslint-config-aqua": "^1.4.1"
},
"eslintConfig": {
"extends": [
"aqua"
],
"env": {
"browser": true,
"node": true
},
"rules": {
"func-names": 0
}
}
"name": "chibisafe",
"version": "4.0.0",
"description": "Blazing fast file uploader and bunker written in node! 🚀",
"license": "MIT",
"author": {
"name": "Pitu",
"email": "heyitspitu@gmail.com",
"url": "https://github.com/Pitu"
},
"scripts": {
"setup": "node src/setup.js && npm run migrate && npm run seed && npm run build",
"build": "nuxt build",
"start": "cross-env NODE_ENV=production node src/api/structures/Server",
"dev": "nuxt",
"migrate": "knex migrate:latest",
"seed": "knex seed:run",
"api": "node src/api/structures/Server",
"update": "git pull && npm install && npm run migrate && npm run build && npm run restart",
"restart": "pm2 restart lolisafe",
"test:vue": "jest --testPathPattern=src/site",
"test:api": "jest --testPathPattern=src/tests/api",
"test:e2e": "jest --testPathPattern=src/tests/e2e",
"tests": "npm run test:api && npm run test:vue && npm run test:e2e"
},
"repository": {
"type": "git",
"url": "https://github.com/WeebDev/chibisafe"
},
"bugs": {
"url": "https://github.com/WeebDev/chibisafe/issues"
},
"engines": {
"node": ">=12.0.0"
},
"dependencies": {
"@mdi/font": "^5.3.45",
"@nuxtjs/axios": "^5.4.1",
"adm-zip": "^0.4.13",
"bcrypt": "^5.0.0",
"body-parser": "^1.18.3",
"buefy": "^0.9.4",
"busboy": "^0.2.14",
"chalk": "^2.4.1",
"chrono-node": "^2.1.4",
"compression": "^1.7.2",
"cookie-universal-nuxt": "^2.0.14",
"cors": "^2.8.5",
"dotenv": "^6.2.0",
"dumper.js": "^1.3.1",
"express": "^4.17.1",
"express-rate-limit": "^3.4.0",
"ffmpeg-probe": "^1.0.6",
"file-saver": "^2.0.1",
"fluent-ffmpeg": "^2.1.2",
"fs-jetpack": "^2.2.2",
"helmet": "^3.15.1",
"imagesloaded": "^4.1.4",
"jsonwebtoken": "^8.5.0",
"knex": "^0.16.3",
"masonry-layout": "^4.2.2",
"moment": "^2.24.0",
"morgan": "^1.10.0",
"multer": "^1.4.1",
"mysql": "^2.16.0",
"nuxt": "2.12.2",
"nuxt-dropzone": "^0.2.8",
"pg": "^7.8.1",
"qoa": "^0.2.0",
"randomstring": "^1.1.5",
"rotating-file-stream": "^2.1.3",
"search-query-parser": "^1.5.5",
"serve-static": "^1.13.2",
"sharp": "^0.27.0",
"sqlite3": "^5.0.0",
"uuid": "^3.3.2",
"v-clipboard": "^2.2.1",
"vue-axios": "^2.1.4",
"vue-isyourpasswordsafe": "^1.0.2",
"vue-plyr": "^5.1.0",
"vue-timeago": "^3.4.4",
"vue2-transitions": "^0.2.3",
"vuebar": "^0.0.20"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@vue/test-utils": "^1.1.2",
"autoprefixer": "^9.4.7",
"axios": "^0.21.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^26.6.3",
"cross-env": "^5.2.0",
"eslint": "^7.17.0",
"eslint-config-aqua": "^7.3.0",
"eslint-import-resolver-nuxt": "^1.0.1",
"eslint-plugin-vue": "^5.2.1",
"jest": "^26.6.3",
"jest-serializer-vue": "^2.0.2",
"node-sass": "^5.0.0",
"nodemon": "^1.19.3",
"postcss-css-variables": "^0.11.0",
"postcss-nested": "^3.0.0",
"puppeteer": "^5.5.0",
"sass-loader": "^10.1.0",
"vue-jest": "^3.0.7"
},
"eslintConfig": {
"extends": [
"aqua/node",
"aqua/vue"
],
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module"
},
"settings": {
"import/resolver": {
"nuxt": {
"nuxtSrcDir": "./src/site",
"extensions": [
".js",
".vue"
]
}
}
}
},
"keywords": [
"chibisafe",
"lolisafe",
"upload",
"uploader",
"file",
"vue",
"ssr",
"file uploader",
"images"
]
}

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - 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">
<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="/js/album.js"></script>
</head>
<body>
<section class="hero is-fullheight">
<div class="hero-head">
<div class="container">
<h1 class="title" id='title' style='margin-top: 1.5rem;'></h1>
<h1 class="subtitle" id='count'></h1>
<hr>
</div>
</div>
<div class="hero-body">
<div class="container" id='container'>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - 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">
<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,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - 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">
<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/dashboard.js"></script>
</head>
<body>
<section id='auth' class="hero is-light is-fullheight">
<div class="hero-body">
<div class="container">
<h1 class="title">
Admin dashboard
</h1>
<h2 class="subtitle">
<p class="control has-addons">
<input id='token' class="input is-danger" type="text" placeholder="Your admin token">
<a id='tokenSubmit' class="button is-danger is-outlined">Check</a>
</p>
</h2>
</div>
</div>
</section>
<section id='dashboard' class="section">
<div id="panel" class="container">
<h1 class="title">Dashboard</h1>
<h2 class="subtitle">A simple <strong>dashboard</strong>, to sort your uploaded stuff</h2>
<hr>
<div class="columns">
<div class="column is-3">
<aside class="menu" id="menu">
<p class="menu-label">General</p>
<ul class="menu-list">
<li><a href="/">Frontpage</a></li>
<li><a id="itemUploads" onclick="panel.getUploads()">Uploads</a></li>
</ul>
<p class="menu-label">Albums</p>
<ul class="menu-list">
<li><a id="itemManageGallery" onclick="panel.getAlbums()">Manage your albums</a></li>
<li>
<ul id='albumsContainer'></ul>
</li>
</ul>
<p class="menu-label">Administration</p>
<ul class="menu-list">
<li><a id="itemTokens" onclick="panel.changeToken()">Change your token</a></li>
<li><a id="itemPassword" onclick="panel.changePassword()">Change your password</a></li>
<li><a id="itemLogout"onclick="panel.logout()">Logout</a></li>
</ul>
</aside>
</div>
<div class="column has-text-centered" id='page'>
<img src="/images/logo.png">
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>lolisafe</title>
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">Page not found.</div>
</div>
</div>
</body>
</html>

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>lolisafe</title>
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">Internal server error.</div>
</div>
</div>
</body>
</html>

View File

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - 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="/css/style.css">
</head>
<body>
<section class="hero is-fullheight has-text-centered" id="home">
<div class="hero-body">
<div class="container has-text-left">
<h2 class='subtitle'>What is lolisafe?</h2>
<article class="message">
<div class="message-body">
lolisafe is an easy to use, open source and completely free file upload service. We accept your files, photos, documents, anything, and give you back a shareable link for you to send to others.
</div>
</article>
<h2 class='subtitle'>Will you keep my files forever?</h2>
<article class="message">
<div class="message-body">
Unless we receive a copyright complain or some other bullshit, we will.
</div>
</article>
<h2 class='subtitle'>How can I keep track of my uploads?</h2>
<article class="message">
<div class="message-body">
Simply create a user on the site and every upload will be associated with your account, granting you access to your uploaded files through our dashboard.
</div>
</article>
<h2 class='subtitle'>What are albums?</h2>
<article class="message">
<div class="message-body">
Albums are a simple way of sorting uploads together. Right now you can create albums through the dashboard and use them only with <a target="_blank" href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">our chrome extension</a> which will enable you to <strong>right click -> send to lolisafe</strong> or to a desired album if you have any.
</div>
</article>
<h2 class='subtitle'>Why should I use this?</h2>
<article class="message">
<div class="message-body">
There are too many file upload services out there, and a lot of them rely on the foundations of pomf which is ancient. In a desperate and unsuccessful attempt of finding a good file uploader that's easily extendable, lolisafe was born. We give you control over your files, we give you a way to sort your uploads into albums for ease of access and we give you an api to use with ShareX or any other thing that let's you make POST requests. Awesome isn't it? Just like you.
</div>
</article>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,93 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - 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">
<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/dropzone/4.3.0/min/dropzone.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="/js/home.js"></script>
</head>
<body>
<section class="hero is-fullheight has-text-centered" id="home">
<div class="hero-body">
<div class="container">
<p id="b">
<img class="logo" src="/images/logo_smol.png">
</p>
<h1 class="title">lolisafe</h1>
<h2 class="subtitle">A <strong>modern</strong> self-hosted file upload service</h2>
<h3 class="subtitle" id="maxFileSize"></h3>
<div class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column" id="uploadContainer">
<a id="loginToUpload" href="/auth" class="button is-danger">Running in private mode. Log in to upload.</a>
<div class="field" id="albumDiv" style="display: none">
<p class="control select-wrapper">
<span class="select">
<select id="albumSelect">
<option value="">Upload to album</option>
</select>
</span>
</p>
</div>
</div>
<div class="column is-hidden-mobile"></div>
</div>
<div id="uploads">
<div id="template" class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column">
<progress class="progress is-small is-danger" value="0" max="100" data-dz-uploadprogress></progress>
<p data-dz-errormessage></p>
<p class="link"></p>
</div>
<div class="column is-hidden-mobile"></div>
</div>
</div>
<h3 class="subtitle"><a href="/auth" id="loginLinkText"></a></h3>
<h3 id="links">
<a href="https://github.com/WeebDev/lolisafe" target="_blank" class="is-danger">View on GitHub</a><span>|</span><a id="ShareX" href="https://lolisafe.moe/sharex.txt">ShareX</a><span>|</span><a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj" target="_blank" class="is-danger">Chrome extension</a><span>|</span><a href="/faq" class="is-danger">FAQ</a><span>|</span><a href="/auth" target="_blank" class="is-danger">Dashboard</a>
</h3>
</div>
</div>
</section>
</body>
</html>

15
pm2.json Normal file
View File

@ -0,0 +1,15 @@
{
"apps": [
{
"name": "chibisafe",
"script": "npm",
"args": "run start",
"env": {
"NODE_ENV": "production"
},
"env_production": {
"NODE_ENV": "production"
}
}
]
}

View File

@ -1,113 +0,0 @@
/* ------------------
HOME
------------------ */
section#home #b {
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
-webkit-animation-duration: 1.5s;
animation-duration: 1.5s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: floatUp;
animation-name: floatUp;
-webkit-animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
border-radius: 24px;
display: inline-block;
height: 240px;
margin-bottom: 40px;
position: relative;
vertical-align: top;
width: 240px;
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
}
section#home div#dropzone {
border: 1px solid #dbdbdb;
background-color: rgba(0, 0, 0, 0);
border-color: #ff3860;
color: #ff3860;
display: none;
width: 100%;
border-radius: 3px;
box-shadow: none;
height: 2.5em;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
user-select: none;
justify-content: center;
padding-left: .75em;
padding-right: .75em;
text-align: center;
cursor: pointer;
}
section#home div#uploads, section#home p#tokenContainer, section#home a#panel { display: none; }
section#home div#dropzone:hover { background-color: #ff3860; border-color: #ff3860; color: #fff; }
section#home h3#maxFileSize { font-size: 14px; }
section#home h3#links span { padding-left: 5px; padding-right: 5px; }
section#home img.logo { height: 200px; margin-top: 20px; }
section#home .dz-preview .dz-details { display: flex; }
section#home .dz-preview .dz-details .dz-size, section#home .dz-preview .dz-details .dz-filename { flex: 1; }
section#home .dz-preview img, section#home .dz-preview .dz-success-mark, section#home .dz-preview .dz-error-mark { display: none; }
section#home div#uploads { margin-bottom: 25px; }
@keyframes floatUp {
0% {
opacity: 0;
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0);
-webkit-transform: scale(0.86);
transform: scale(0.86);
}
25% { opacity: 100; }
67% {
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
-webkit-transform: scale(1);
transform: scale(1);
}
100% {
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
-webkit-transform: scale(1);
transform: scale(1);
}
}
/* ------------------
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 {
border-left: 0px;
border-top: 0px;
border-right: 0px;
border-radius: 0px;
box-shadow: 0 0 0;
}
section#dashboard .table { font-size: 12px }
section#dashboard div#table div.column { display:flex; width: 200px; height: 220px; margin: 9px; background: #f9f9f9; overflow: hidden; flex-wrap: wrap; align-items: center; }
section#dashboard div#table div.column a { width: 100%; }
section#dashboard div#table div.column a:first-child { height: 180px; }
section#dashboard div#table div.column a img { width:200px; }
.select-wrapper {
text-align: center;
margin-bottom: 10px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,47 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.003765,-0.003765)"
fill="#000000" stroke="none">
<path d="M2397 4143 c-3 -3 -19 -7 -34 -8 -162 -16 -259 -56 -351 -144 -65
-64 -123 -152 -156 -239 -22 -61 -27 -80 -40 -148 -6 -29 -18 -161 -20 -209
-1 -40 -2 -40 -81 -80 l-81 -40 58 -50 c32 -28 70 -55 85 -62 l28 -11 -25 -10
-25 -10 29 -13 28 -13 -6 -106 c-3 -58 -7 -114 -9 -125 -3 -11 -8 -45 -11 -76
-4 -31 -9 -67 -11 -80 -3 -13 -12 -60 -20 -104 -21 -108 -99 -327 -177 -497
-29 -64 -32 -67 -62 -64 -17 2 -36 4 -42 5 -25 2 -95 171 -108 259 -9 62 -14
55 -73 -98 -35 -89 -48 -133 -59 -190 -2 -14 -6 -34 -9 -45 -21 -99 -22 -255
-2 -370 8 -44 14 -81 13 -82 -5 -6 -97 76 -131 117 -47 57 -97 151 -111 205
-30 123 -36 217 -19 300 8 41 -6 24 -48 -60 -36 -72 -72 -169 -81 -221 -3 -16
-10 -44 -16 -63 -10 -36 -7 -255 4 -304 4 -16 15 -51 26 -79 11 -28 17 -54 15
-58 -14 -22 -177 11 -235 48 l-25 16 16 -25 c27 -42 141 -127 204 -153 33 -14
86 -32 117 -41 48 -14 58 -20 63 -44 3 -15 31 -65 61 -112 31 -46 68 -109 84
-139 15 -31 41 -67 57 -80 16 -14 45 -45 64 -70 19 -25 37 -47 40 -50 3 -3 34
-43 69 -90 109 -146 240 -259 412 -353 25 -14 65 -38 89 -53 48 -30 175 -79
229 -88 117 -19 211 -37 250 -49 47 -14 85 -14 160 -3 131 21 171 87 171 281
-1 73 -3 93 -12 110 -4 6 -9 22 -13 37 -6 26 -54 109 -159 278 -26 41 -55 85
-63 97 -14 20 -14 23 -2 23 9 0 32 -21 52 -47 99 -128 366 -377 506 -473 12
-8 43 -30 68 -49 38 -29 57 -35 116 -41 128 -13 166 5 214 103 81 161 76 270
-19 438 -23 41 -45 76 -48 79 -3 3 -31 34 -61 70 -30 36 -57 67 -61 70 -15 13
-169 222 -169 231 0 6 7 23 15 38 26 51 56 185 61 271 3 47 7 93 9 103 5 27
-8 20 -38 -22 -15 -22 -45 -54 -67 -71 l-40 -32 -1 89 c0 48 -3 81 -7 73 -11
-25 -41 -64 -46 -59 -2 2 -7 33 -11 69 -7 65 -7 66 22 80 15 7 30 14 33 14 3
1 26 13 52 28 56 34 121 97 157 153 22 35 30 41 47 34 47 -17 103 -115 91
-160 -6 -22 8 -26 17 -4 3 8 8 36 12 63 10 79 -28 143 -98 165 -15 5 -15 10
-4 49 31 104 15 279 -35 384 -19 40 -29 78 -29 107 0 48 -12 61 -43 45 -9 -5
-23 -5 -31 -1 -31 19 -74 61 -91 91 -13 21 -35 38 -62 48 -24 10 -42 22 -40
28 3 6 9 10 15 9 9 -2 52 84 67 137 4 14 25 42 47 62 22 20 37 42 33 48 -3 5
-11 7 -17 4 -6 -4 -6 1 1 14 6 12 16 24 22 27 6 4 23 25 39 47 29 40 29 41 10
57 -17 13 -18 22 -11 54 5 21 7 50 6 65 -2 15 -6 52 -9 82 -10 114 -77 279
-158 387 -46 63 -167 162 -247 202 -97 49 -263 91 -276 70 -4 -5 -13 -5 -24 1
-10 5 -21 7 -24 3z"/>
<path d="M3111 3011 c-13 -13 -21 -35 -22 -58 0 -21 -1 -53 -2 -71 -1 -17 2
-32 6 -32 4 0 29 11 55 24 74 37 87 86 27 98 -12 2 -19 10 -17 20 1 8 -4 21
-12 27 -12 10 -19 8 -35 -8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

@ -1,56 +0,0 @@
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 = '/dashboard';
})
.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 = '/dashboard';
})
.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,620 +0,0 @@
let panel = {};
panel.page;
panel.username;
panel.token = localStorage.token;
panel.filesView = localStorage.filesView;
panel.preparePage = function(){
if(!panel.token) return window.location = '/auth';
panel.verifyToken(panel.token, true);
};
panel.verifyToken = function(token, reloadOnError){
if(reloadOnError === undefined)
reloadOnError = false;
axios.post('/api/tokens/verify', {
token: token
})
.then(function (response) {
if(response.data.success === false){
swal({
title: "An error ocurred",
text: response.data.description,
type: "error"
}, function(){
if(reloadOnError){
localStorage.removeItem("token");
location.location = '/auth';
}
});
return;
}
axios.defaults.headers.common['token'] = token;
localStorage.token = token;
panel.token = token;
panel.username = response.data.username;
return panel.prepareDashboard();
})
.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);
});
};
panel.prepareDashboard = function(){
panel.page = document.getElementById('page');
document.getElementById('auth').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
document.getElementById('itemUploads').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemManageGallery').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemTokens').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemPassword').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemLogout').innerHTML = `Logout ( ${panel.username} )`;
panel.getAlbumsSidebar();
};
panel.logout = function(){
localStorage.removeItem("token");
location.reload('/');
};
panel.getUploads = function(album = undefined, page = undefined){
if(page === undefined) page = 0;
let url = '/api/uploads/' + page;
if(album !== undefined)
url = '/api/album/' + album + '/' + page;
axios.get(url).then(function (response) {
if(response.data.success === false){
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;
if(response.data.files.length < 25)
nextPage = page;
if(page > 0) prevPage = page - 1;
panel.page.innerHTML = '';
var container = document.createElement('div');
var pagination = `<nav class="pagination is-centered">
<a class="pagination-previous" onclick="panel.getUploads(${album}, ${prevPage} )">Previous</a>
<a class="pagination-next" onclick="panel.getUploads(${album}, ${nextPage} )">Next page</a>
</nav>`;
var listType = `
<div class="columns">
<div class="column">
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('list', ${album}, ${page})">
<span class="icon is-small">
<i class="fa fa-list-ul"></i>
</span>
</a>
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('thumbs', ${album}, ${page})">
<span class="icon is-small">
<i class="fa fa-th-large"></i>
</span>
</a>
</div>
</div>`;
if(panel.filesView === 'thumbs'){
container.innerHTML = `
${pagination}
<hr>
${listType}
<div class="columns is-multiline is-mobile" id="table">
</div>
${pagination}
`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.files){
var div = document.createElement('div');
div.className = "column is-2";
if(item.thumb !== undefined)
div.innerHTML = `<a href="${item.file}" target="_blank"><img src="${item.thumb}"/></a><a class="button is-small is-danger is-outlined" title="Delete file" onclick="panel.deleteFile(${item.id})"><span class="icon is-small"><i class="fa fa-trash-o"></i></span></a>`;
else
div.innerHTML = `<a href="${item.file}" target="_blank"><h1 class="title">.${item.file.split('.').pop()}</h1></a><a class="button is-small is-danger is-outlined" title="Delete file" onclick="panel.deleteFile(${item.id})"><span class="icon is-small"><i class="fa fa-trash-o"></i></span></a>`;
table.appendChild(div);
}
}else{
var albumOrUser = 'Album';
if(panel.username === 'root')
albumOrUser = 'User';
container.innerHTML = `
${pagination}
<hr>
${listType}
<table class="table is-striped is-narrow is-left">
<thead>
<tr>
<th>File</th>
<th>${albumOrUser}</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody id="table">
</tbody>
</table>
<hr>
${pagination}
`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.files){
var tr = document.createElement('tr');
var displayAlbumOrUser = item.album;
if(panel.username === 'root'){
displayAlbumOrUser = '';
if(item.username !== undefined)
displayAlbumOrUser = item.username;
}
tr.innerHTML = `
<tr>
<th><a href="${item.file}" target="_blank">${item.file}</a></th>
<th>${displayAlbumOrUser}</th>
<td>${item.date}</td>
<td>
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteFile(${item.id})">
<span class="icon is-small">
<i class="fa fa-trash-o"></i>
</span>
</a>
</td>
</tr>
`;
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");
console.log(error);
});
};
panel.setFilesView = function(view, album, page){
localStorage.filesView = view;
panel.filesView = view;
panel.getUploads(album, page);
};
panel.deleteFile = function(id){
swal({
title: "Are you sure?",
text: "You wont be able to recover the file!",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#ff3860",
confirmButtonText: "Yes, delete it!",
closeOnConfirm: false
},
function(){
axios.post('/api/upload/delete', {
id: id
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Deleted!", "The file has been deleted.", "success");
panel.getUploads();
})
.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);
});
}
);
};
panel.getAlbums = function(){
axios.get('/api/albums').then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Create new album</h2>
<p class="control has-addons has-addons-centered">
<input id="albumName" class="input" type="text" placeholder="Name">
<a id="submitAlbum" class="button is-primary">Submit</a>
</p>
<h2 class="subtitle">List of albums</h2>
<table class="table is-striped is-narrow">
<thead>
<tr>
<th>Name</th>
<th>Files</th>
<th>Created At</th>
<th>Public link</th>
<th></th>
</tr>
</thead>
<tbody id="table">
</tbody>
</table>`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.albums){
var tr = document.createElement('tr');
tr.innerHTML = `
<tr>
<th>${item.name}</th>
<th>${item.files}</th>
<td>${item.date}</td>
<td><a href="${item.identifier}" target="_blank">Album link</a></td>
<td>
<a class="button is-small is-primary is-outlined" title="Edit name" onclick="panel.renameAlbum(${item.id})">
<span class="icon is-small">
<i class="fa fa-pencil"></i>
</span>
</a>
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteAlbum(${item.id})">
<span class="icon is-small">
<i class="fa fa-trash-o"></i>
</span>
</a>
</td>
</tr>
`;
table.appendChild(tr);
}
document.getElementById('submitAlbum').addEventListener('click', 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");
console.log(error);
});
};
panel.renameAlbum = function(id){
swal({
title: "Rename album",
text: "New name you want to give the album:",
type: "input",
showCancelButton: true,
closeOnConfirm: false,
animation: "slide-from-top",
inputPlaceholder: "My super album"
},function(inputValue){
if (inputValue === false) return false;
if (inputValue === "") {
swal.showInputError("You need to write something!");
return false;
}
axios.post('/api/albums/rename', {
id: id,
name: inputValue
})
.then(function (response) {
if(response.data.success === false){
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;
}
swal("Success!", "Your album was renamed to: " + inputValue, "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.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);
});
});
};
panel.deleteAlbum = function(id){
swal({
title: "Are you sure?",
text: "This won't delete your files, only the album!",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#ff3860",
confirmButtonText: "Yes, delete it!",
closeOnConfirm: false
},
function(){
axios.post('/api/albums/delete', {
id: id
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Deleted!", "Your album has been deleted.", "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.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);
});
}
);
};
panel.submitAlbum = function(){
axios.post('/api/albums', {
name: document.getElementById('albumName').value
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Woohoo!", "Album was added successfully", "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.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);
});
};
panel.getAlbumsSidebar = function(){
axios.get('/api/albums/sidebar')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
var albumsContainer = document.getElementById('albumsContainer');
albumsContainer.innerHTML = '';
if(response.data.albums === undefined) return;
for(var album of response.data.albums){
li = document.createElement('li');
a = document.createElement('a');
a.id = album.id;
a.innerHTML = album.name;
a.addEventListener('click', function(){
panel.getAlbum(this);
});
li.appendChild(a);
albumsContainer.appendChild(li);
}
})
.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);
});
};
panel.getAlbum = function(item){
panel.setActiveMenu(item);
panel.getUploads(item.id);
};
panel.changeToken = function(){
axios.get('/api/tokens')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Manage your token</h2>
<label class="label">Your current token:</label>
<p class="control has-addons">
<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('getNewToken').addEventListener('click', function(){
panel.getNewToken();
});
})
.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);
});
};
panel.getNewToken = function(){
axios.post('/api/tokens/change')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal({
title: "Woohoo!",
text: 'Your token was changed successfully.',
type: "success"
}, function(){
localStorage.token = response.data.token;
location.reload();
});
})
.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);
});
};
panel.changePassword = function(){
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Change your password</h2>
<label class="label">New password:</label>
<p class="control has-addons">
<input id="password" class="input is-expanded" type="password" placeholder="Your new password">
</p>
<label class="label">Confirm password:</label>
<p class="control has-addons">
<input id="passwordConfirm" class="input is-expanded" type="password" placeholder="Verify your new password">
<a id="sendChangePassword" class="button is-primary">Set new password</a>
</p>
`;
panel.page.appendChild(container);
document.getElementById('sendChangePassword').addEventListener('click', function(){
if (document.getElementById('password').value === document.getElementById('passwordConfirm').value) {
panel.sendNewPassword(document.getElementById('password').value);
} else {
swal({
title: "Password mismatch!",
text: 'Your passwords do not match, please try again.',
type: "error"
}, function() {
panel.changePassword();
});
}
});
};
panel.sendNewPassword = function(pass){
axios.post('/api/password/change', {password: pass})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal({
title: "Woohoo!",
text: 'Your password was changed successfully.',
type: "success"
}, function(){
location.reload();
});
})
.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);
});
};
panel.setActiveMenu = function(item){
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('a');
for(var i = 0; i < items.length; i++)
items[i].className = "";
item.className = 'is-active';
};
window.onload = function () {
panel.preparePage();
};

View File

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

View File

@ -1,30 +0,0 @@
# https://www.cloudflare.com/ips/
# IPv4 Ranges
# https://www.cloudflare.com/ips-v4/
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
# IPv6 Ranges
# https://www.cloudflare.com/ips-v6/
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;
real_ip_header CF-Connecting-IP;

View File

@ -1,56 +0,0 @@
const config = require('../config.js');
const routes = require('express').Router();
const db = require('knex')(config.database);
const path = require('path');
const utils = require('../controllers/utilsController.js');
routes.get('/a/:identifier', async (req, res, next) => {
let identifier = req.params.identifier;
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();
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');
let thumb = '';
const basedomain = config.domain;
for (let file of files) {
file.file = `${basedomain}/${file.name}`;
let ext = path.extname(file.name).toLowerCase();
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`;
/*
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
since embedding the first image could potentially result in nsfw content when pasting links.
*/
if (thumb === '') {
thumb = file.thumb;
}
file.thumb = `<img src="${file.thumb}"/>`;
} else {
file.thumb = `<h1 class="title">.${ext}</h1>`;
}
}
let enableDownload = false;
if (config.uploads.generateZips) enableDownload = true;
return res.render('album', {
layout: false,
title: album.name,
count: files.length,
thumb,
files,
identifier,
enableDownload
});
});
module.exports = routes;

View File

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

View File

@ -0,0 +1,93 @@
exports.up = async knex => {
await knex.schema.createTable('users', table => {
table.increments();
table.string('username');
table.text('password');
table.boolean('enabled');
table.boolean('isAdmin');
table.string('apiKey');
table.timestamp('passwordEditedAt');
table.timestamp('apiKeyEditedAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
await knex.schema.createTable('albums', table => {
table.increments();
table.integer('userId');
table.string('name');
table.timestamp('zippedAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
await knex.schema.createTable('files', table => {
table.increments();
table.integer('userId');
table.string('name');
table.string('original');
table.string('type');
table.integer('size');
table.string('hash');
table.string('ip');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
await knex.schema.createTable('links', table => {
table.increments();
table.integer('userId');
table.integer('albumId');
table.string('identifier');
table.integer('views');
table.boolean('enabled');
table.boolean('enableDownload');
table.timestamp('expiresAt');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
await knex.schema.createTable('albumsFiles', table => {
table.increments();
table.integer('albumId');
table.integer('fileId');
});
await knex.schema.createTable('albumsLinks', table => {
table.increments();
table.integer('albumId');
table.integer('linkId');
});
await knex.schema.createTable('tags', table => {
table.increments();
table.string('uuid');
table.integer('userId');
table.string('name');
table.timestamp('createdAt');
table.timestamp('editedAt');
});
await knex.schema.createTable('fileTags', table => {
table.increments();
table.integer('fileId');
table.integer('tagId');
});
await knex.schema.createTable('bans', table => {
table.increments();
table.string('ip');
table.timestamp('createdAt');
});
};
exports.down = async knex => {
await knex.schema.dropTableIfExists('users');
await knex.schema.dropTableIfExists('albums');
await knex.schema.dropTableIfExists('files');
await knex.schema.dropTableIfExists('links');
await knex.schema.dropTableIfExists('albumsFiles');
await knex.schema.dropTableIfExists('albumsLinks');
await knex.schema.dropTableIfExists('tags');
await knex.schema.dropTableIfExists('fileTags');
await knex.schema.dropTableIfExists('bans');
};

View File

@ -0,0 +1,33 @@
exports.up = async knex => {
await knex.schema.alterTable('users', table => {
table.unique(['username', 'apiKey']);
});
await knex.schema.alterTable('albums', table => {
table.boolean('nsfw').defaultTo(false);
table.unique(['userId', 'name']);
});
await knex.schema.alterTable('links', table => {
table.unique(['userId', 'albumId', 'identifier']);
});
await knex.schema.alterTable('albumsFiles', table => {
table.unique(['albumId', 'fileId']);
});
await knex.schema.alterTable('albumsLinks', table => {
table.unique(['linkId']);
});
await knex.schema.alterTable('tags', table => {
table.unique(['userId', 'name']);
});
await knex.schema.alterTable('fileTags', table => {
table.unique(['fileId', 'tagId']);
});
};
exports.down = async knex => {
// Nothing
};

View File

@ -0,0 +1,30 @@
/* eslint-disable no-console */
const bcrypt = require('bcrypt');
const moment = require('moment');
exports.seed = async db => {
const now = moment.utc().toDate();
const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first();
if (user) return;
try {
const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
await db.table('users').insert({
username: process.env.ADMIN_ACCOUNT,
password: hash,
passwordEditedAt: now,
createdAt: now,
editedAt: now,
enabled: true,
isAdmin: true
});
console.log();
console.log('=========================================================');
console.log('== Successfully created the admin account. ==');
console.log('=========================================================');
console.log('== Run `pm2 start pm2.json` to start the service ==');
console.log('=========================================================');
console.log();
} catch (error) {
console.error(error);
}
};

View File

@ -0,0 +1,136 @@
const nodePath = require('path');
const moment = require('moment');
const jetpack = require('fs-jetpack');
const ThumbUtil = require('./utils/ThumbUtil');
const oldDb = require('knex')({
client: 'sqlite3',
connection: {
filename: nodePath.join(__dirname, '../../', 'db')
},
useNullAsDefault: true
});
const newDb = require('knex')({
client: 'sqlite3',
connection: {
filename: nodePath.join(__dirname, '../../database/', 'database.sqlite')
},
postProcessResponse: result => {
const booleanFields = [
'enabled',
'enableDownload',
'isAdmin',
'nsfw'
];
const processResponse = row => {
Object.keys(row).forEach(key => {
if (booleanFields.includes(key)) {
if (row[key] === 0) row[key] = false;
else if (row[key] === 1) row[key] = true;
}
});
return row;
};
if (Array.isArray(result)) return result.map(row => processResponse(row));
if (typeof result === 'object') return processResponse(result);
return result;
},
useNullAsDefault: true
});
const start = async () => {
console.log('Starting migration, this may take a few minutes...'); // Because I half assed it
console.log('Please do NOT kill the process. Wait for it to finish.');
await jetpack.removeAsync(nodePath.join(__dirname, '../../uploads/thumbs'));
await jetpack.dirAsync(nodePath.join(__dirname, '../../uploads/thumbs/square'));
console.log('Finished deleting old thumbnails to create new ones');
const users = await oldDb.table('users').where('username', '<>', 'root');
for (const user of users) {
const now = moment.utc().toDate();
const userToInsert = {
id: user.id,
username: user.username,
password: user.password,
enabled: user.enabled == 1,
isAdmin: false,
apiKey: user.token,
passwordEditedAt: now,
apiKeyEditedAt: now,
createdAt: now,
editedAt: now
};
await newDb.table('users').insert(userToInsert);
}
console.log('Finished migrating users...');
const albums = await oldDb.table('albums');
for (const album of albums) {
if (!album.enabled || album.enabled == 0) continue;
const now = moment.utc().toDate();
const albumToInsert = {
id: album.id,
userId: album.userid,
name: album.name,
zippedAt: album.zipGeneratedAt ? moment.unix(album.zipGeneratedAt).toDate() : null,
createdAt: moment.unix(album.timestamp).toDate(),
editedAt: moment.unix(album.editedAt).toDate()
};
const linkToInsert = {
userId: album.userid,
albumId: album.id,
identifier: album.identifier,
views: 0,
enabled: true,
enableDownload: true,
createdAt: now,
editedAt: now
};
await newDb.table('albums').insert(albumToInsert);
const insertedId = await newDb.table('links').insert(linkToInsert);
await newDb.table('albumsLinks').insert({
albumId: album.id,
linkId: insertedId[0]
});
}
console.log('Finished migrating albums...');
const files = await oldDb.table('files');
const filesToInsert = [];
const albumsFilesToInsert = [];
for (const file of files) {
const fileToInsert = {
id: file.id,
userId: file.userid,
name: file.name,
original: file.original,
type: file.type,
size: file.size,
hash: file.hash,
ip: file.ip,
createdAt: moment.unix(file.timestamp).toDate(),
editedAt: moment.unix(file.timestamp).toDate()
};
filesToInsert.push(fileToInsert);
albumsFilesToInsert.push({
albumId: file.albumid,
fileId: file.id
});
const filename = file.name;
if (!jetpack.exists(nodePath.join(__dirname, '../../uploads', filename))) continue;
ThumbUtil.generateThumbnails(filename);
}
await newDb.batchInsert('files', filesToInsert, 20);
await newDb.batchInsert('albumsFiles', albumsFilesToInsert, 20);
console.log('Finished migrating files...');
console.log('Finished migrating everything. ');
process.exit(0);
};
start();

View File

@ -0,0 +1,25 @@
const Route = require('../../structures/Route');
class banIP extends Route {
constructor() {
super('/admin/ban/ip', 'post', { adminOnly: true });
}
async run(req, res, db) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { ip } = req.body;
if (!ip) return res.status(400).json({ message: 'No ip provided' });
try {
await db.table('bans').insert({ ip });
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully banned the ip'
});
}
}
module.exports = banIP;

View File

@ -0,0 +1,32 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class filesGET extends Route {
constructor() {
super('/admin/file/:id', 'get', { adminOnly: true });
}
async run(req, res, db) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
let file = await db.table('files').where({ id }).first();
const user = await db.table('users')
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
.where({ id: file.userId })
.first();
file = Util.constructFilePublicLink(file);
// Additional relevant data
const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
user.fileCount = filesFromUser.length;
return res.json({
message: 'Successfully retrieved file',
file,
user
});
}
}
module.exports = filesGET;

View File

@ -0,0 +1,27 @@
const Route = require('../../structures/Route');
class unBanIP extends Route {
constructor() {
super('/admin/unban/ip', 'post', { adminOnly: true });
}
async run(req, res, db) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { ip } = req.body;
if (!ip) return res.status(400).json({ message: 'No ip provided' });
try {
await db.table('bans')
.where({ ip })
.delete();
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully unbanned the ip'
});
}
}
module.exports = unBanIP;

View File

@ -0,0 +1,28 @@
const Route = require('../../structures/Route');
class userDemote extends Route {
constructor() {
super('/admin/users/demote', 'post', { adminOnly: true });
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id } = req.body;
if (!id) return res.status(400).json({ message: 'No id provided' });
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
try {
await db.table('users')
.where({ id })
.update({ isAdmin: false });
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully demoted user'
});
}
}
module.exports = userDemote;

View File

@ -0,0 +1,28 @@
const Route = require('../../structures/Route');
class userDisable extends Route {
constructor() {
super('/admin/users/disable', 'post', { adminOnly: true });
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id } = req.body;
if (!id) return res.status(400).json({ message: 'No id provided' });
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
try {
await db.table('users')
.where({ id })
.update({ enabled: false });
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully disabled user'
});
}
}
module.exports = userDisable;

View File

@ -0,0 +1,28 @@
const Route = require('../../structures/Route');
class userEnable extends Route {
constructor() {
super('/admin/users/enable', 'post', { adminOnly: true });
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id } = req.body;
if (!id) return res.status(400).json({ message: 'No id provided' });
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
try {
await db.table('users')
.where({ id })
.update({ enabled: true });
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully enabled user'
});
}
}
module.exports = userEnable;

View File

@ -0,0 +1,37 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class usersGET extends Route {
constructor() {
super('/admin/users/:id', 'get', { adminOnly: true });
}
async run(req, res, db) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' });
try {
const user = await db.table('users')
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
.where({ id })
.first();
const files = await db.table('files')
.where({ userId: user.id })
.orderBy('id', 'desc');
for (let file of files) {
file = Util.constructFilePublicLink(file);
}
return res.json({
message: 'Successfully retrieved user',
user,
files
});
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = usersGET;

View File

@ -0,0 +1,28 @@
const Route = require('../../structures/Route');
class userPromote extends Route {
constructor() {
super('/admin/users/promote', 'post', { adminOnly: true });
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id } = req.body;
if (!id) return res.status(400).json({ message: 'No id provided' });
if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' });
try {
await db.table('users')
.where({ id })
.update({ isAdmin: true });
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully promoted user'
});
}
}
module.exports = userPromote;

View File

@ -0,0 +1,26 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class userDemote extends Route {
constructor() {
super('/admin/users/purge', 'post', { adminOnly: true });
}
async run(req, res) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id } = req.body;
if (!id) return res.status(400).json({ message: 'No id provided' });
try {
await Util.deleteAllFilesFromUser(id);
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully deleted the user\'s files'
});
}
}
module.exports = userDemote;

View File

@ -0,0 +1,23 @@
const Route = require('../../structures/Route');
class usersGET extends Route {
constructor() {
super('/admin/users', 'get', { adminOnly: true });
}
async run(req, res, db) {
try {
const users = await db.table('users')
.select('id', 'username', 'enabled', 'isAdmin', 'createdAt');
return res.json({
message: 'Successfully retrieved users',
users
});
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = usersGET;

View File

@ -0,0 +1,38 @@
const Route = require('../../structures/Route');
class albumDELETE extends Route {
constructor() {
super('/album/:id', 'delete');
}
async run(req, res, db, user) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' });
/*
Check if the album exists
*/
const album = await db.table('albums').where({ id, userId: user.id }).first();
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
try {
// Delete the album
await db.table('albums').where({ id }).delete();
// Delete the relation of any files attached to this album
await db.table('albumsFiles').where({ albumId: id }).delete();
// Delete the relation of any links attached to this album
await db.table('albumsLinks').where({ albumId: id }).delete();
// Delete any album links created for this album
await db.table('links').where({ albumId: id }).delete();
return res.json({ message: 'The album was deleted successfully' });
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = albumDELETE;

View File

@ -0,0 +1,33 @@
const Route = require('../../structures/Route');
class albumEditPOST extends Route {
constructor() {
super('/album/edit', 'post');
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { id, name, nsfw } = req.body;
if (!id) return res.status(400).json({ message: 'Invalid album identifier supplied' });
const album = await db.table('albums').where({ id, userId: user.id }).first();
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
try {
const updateObj = {
name: name || album.name,
nsfw: nsfw === true ? true : nsfw === false ? false : album.nsfw
};
await db
.table('albums')
.where({ id })
.update(updateObj);
return res.json({ message: 'Editing the album was successful', data: updateObj });
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = albumEditPOST;

View File

@ -0,0 +1,58 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class albumGET extends Route {
constructor() {
super('/album/:id/full', 'get');
}
async run(req, res, db, user) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid id supplied' });
const album = await db
.table('albums')
.where({ id, userId: user.id })
.first();
if (!album) return res.status(404).json({ message: 'Album not found' });
let count = 0;
let files = db
.table('albumsFiles')
.where({ albumId: id })
.join('files', 'albumsFiles.fileId', 'files.id')
.select('files.id', 'files.name', 'files.createdAt')
.orderBy('files.id', 'desc');
const { page, limit = 100 } = req.query;
if (page && page >= 0) {
files = await files.offset((page - 1) * limit).limit(limit);
const dbRes = await db
.table('albumsFiles')
.count('* as count')
.where({ albumId: id })
.first();
count = dbRes.count;
} else {
files = await files; // execute the query
count = files.length;
}
// eslint-disable-next-line no-restricted-syntax
for (let file of files) {
file = Util.constructFilePublicLink(file);
}
return res.json({
message: 'Successfully retrieved album',
name: album.name,
files,
count
});
}
}
module.exports = albumGET;

View File

@ -0,0 +1,46 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class albumGET extends Route {
constructor() {
super('/album/:identifier', 'get', { bypassAuth: true });
}
async run(req, res, db) {
const { identifier } = req.params;
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
// Make sure it exists and it's enabled
const link = await db.table('links').where({ identifier, enabled: true }).first();
if (!link) return res.status(404).json({ message: 'The album could not be found' });
// Same with the album, just to make sure is not a deleted album and a leftover link
const album = await db.table('albums').where('id', link.albumId).first();
if (!album) return res.status(404).json({ message: 'Album not found' });
const files = await db.table('albumsFiles')
.where({ albumId: link.albumId })
.join('files', 'albumsFiles.fileId', 'files.id')
.select('files.name', 'files.id')
.orderBy('files.id', 'desc');
// Create the links for each file
// eslint-disable-next-line no-restricted-syntax
for (let file of files) {
file = Util.constructFilePublicLink(file);
}
// Add 1 more view to the link
await db.table('links').where({ identifier }).update('views', Number(link.views) + 1);
return res.json({
message: 'Successfully retrieved files',
name: album.name,
downloadEnabled: link.enableDownload,
isNsfw: album.nsfw,
files
});
}
}
module.exports = albumGET;

View File

@ -0,0 +1,39 @@
const moment = require('moment');
const Route = require('../../structures/Route');
class albumPOST extends Route {
constructor() {
super('/album/new', 'post');
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { name } = req.body;
if (!name) return res.status(400).json({ message: 'No name provided' });
/*
Check that an album with that name doesn't exist yet
*/
const album = await db
.table('albums')
.where({ name, userId: user.id })
.first();
if (album) return res.status(401).json({ message: "There's already an album with that name" });
const now = moment.utc().toDate();
const insertObj = {
name,
userId: user.id,
createdAt: now,
editedAt: now
};
const dbRes = await db.table('albums').insert(insertObj);
insertObj.id = dbRes.pop();
return res.json({ message: 'The album was created successfully', data: insertObj });
}
}
module.exports = albumPOST;

View File

@ -0,0 +1,29 @@
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class albumDELETE extends Route {
constructor() {
super('/album/:id/purge', 'delete');
}
async run(req, res, db, user) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' });
/*
Check if the album exists
*/
const album = await db.table('albums').where({ id, userId: user.id }).first();
if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' });
try {
await Util.deleteAllFilesFromAlbum(id);
await db.table('albums').where({ id }).delete();
return res.json({ message: 'The album was deleted successfully' });
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = albumDELETE;

View File

@ -0,0 +1,89 @@
const path = require('path');
const jetpack = require('fs-jetpack');
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
const log = require('../../utils/Log');
class albumGET extends Route {
constructor() {
super('/album/:identifier/zip', 'get', { bypassAuth: true });
}
async run(req, res, db) {
const { identifier } = req.params;
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
// TODO: Do we really want to let anyone create a zip of an album?
/*
Make sure it exists and it's enabled
*/
const link = await db.table('links')
.where({
identifier,
enabled: true,
enableDownload: true
})
.first();
if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' });
/*
Same with the album, just to make sure is not a deleted album and a leftover link
*/
const album = await db.table('albums')
.where('id', link.albumId)
.first();
if (!album) return res.status(400).json({ message: 'Album not found' });
/*
If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user
*/
if (album.zippedAt > album.editedAt) {
const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`);
const exists = await jetpack.existsAsync(filePath);
/*
Make sure the file exists just in case, and if not, continue to it's generation.
*/
if (exists) {
const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`;
return res.download(filePath, fileName);
}
}
/*
Grab the files in a very unoptimized way. (This should be a join between both tables)
*/
const fileList = await db.table('albumsFiles')
.where('albumId', link.albumId)
.select('fileId');
/*
If there are no files, stop here
*/
if (!fileList || !fileList.length) return res.status(400).json({ message: 'Can\'t download an empty album' });
/*
Get the actual files
*/
const fileIds = fileList.map(el => el.fileId);
const files = await db.table('files')
.whereIn('id', fileIds)
.select('name');
const filesToZip = files.map(el => el.name);
try {
Util.createZip(filesToZip, album);
await db.table('albums')
.where('id', link.albumId)
.update('zippedAt', db.fn.now());
const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`);
const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`;
return res.download(filePath, fileName);
} catch (error) {
log.error(error);
return res.status(500).json({ message: 'There was a problem downloading the album' });
}
}
}
module.exports = albumGET;

View File

@ -0,0 +1,71 @@
/* eslint-disable max-classes-per-file */
const Route = require('../../structures/Route');
const Util = require('../../utils/Util');
class albumsGET extends Route {
constructor() {
super('/albums/mini', 'get');
}
async run(req, res, db, user) {
/*
Let's fetch the albums. This route will only return a small portion
of the album files for displaying on the dashboard. It's probably useless
for anyone consuming the API outside of the chibisafe frontend.
*/
const albums = await db
.table('albums')
.where('albums.userId', user.id)
.select('id', 'name', 'nsfw', 'createdAt', 'editedAt')
.orderBy('createdAt', 'desc');
for (const album of albums) {
// Fetch the total amount of files each album has.
const fileCount = await db // eslint-disable-line no-await-in-loop
.table('albumsFiles')
.where('albumId', album.id)
.count({ count: 'id' });
// Fetch the file list from each album but limit it to 5 per album
const files = await db // eslint-disable-line no-await-in-loop
.table('albumsFiles')
.join('files', { 'files.id': 'albumsFiles.fileId' })
.where('albumId', album.id)
.select('files.id', 'files.name')
.orderBy('albumsFiles.id', 'desc')
.limit(5);
// Fetch thumbnails and stuff
for (let file of files) {
file = Util.constructFilePublicLink(file);
}
album.fileCount = fileCount[0].count;
album.files = files;
}
return res.json({
message: 'Successfully retrieved albums',
albums
});
}
}
class albumsDropdownGET extends Route {
constructor() {
super('/albums/dropdown', 'get', { canApiKey: true });
}
async run(req, res, db, user) {
const albums = await db
.table('albums')
.where('userId', user.id)
.select('id', 'name');
return res.json({
message: 'Successfully retrieved albums',
albums
});
}
}
module.exports = [albumsGET, albumsDropdownGET];

View File

@ -0,0 +1,35 @@
const Route = require('../../../structures/Route');
class linkDELETE extends Route {
constructor() {
super('/album/link/delete/:identifier', 'delete');
}
async run(req, res, db, user) {
const { identifier } = req.params;
if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
try {
const link = await db.table('links')
.where({ identifier, userId: user.id })
.first();
if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist or doesnt\'t belong to the user' });
await db.table('links')
.where({ id: link.id })
.delete();
await db.table('albumsLinks')
.where({ linkId: link.id })
.delete();
} catch (error) {
return super.error(res, error);
}
return res.json({
message: 'Successfully deleted link'
});
}
}
module.exports = linkDELETE;

View File

@ -0,0 +1,38 @@
const Route = require('../../../structures/Route');
class linkEditPOST extends Route {
constructor() {
super('/album/link/edit', 'post');
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { identifier, enableDownload, expiresAt } = req.body;
if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' });
/*
Make sure the link exists
*/
const link = await db
.table('links')
.where({ identifier, userId: user.id })
.first();
if (!link) return res.status(400).json({ message: "The link doesn't exist or doesn't belong to the user" });
try {
const updateObj = {
enableDownload: enableDownload || false,
expiresAt // This one should be null if not supplied
};
await db
.table('links')
.where({ identifier })
.update(updateObj);
return res.json({ message: 'Editing the link was successful', data: updateObj });
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = linkEditPOST;

View File

@ -0,0 +1,78 @@
const Route = require('../../../structures/Route');
const Util = require('../../../utils/Util');
class linkPOST extends Route {
constructor() {
super('/album/link/new', 'post');
}
async run(req, res, db, user) {
if (!req.body) return res.status(400).json({ message: 'No body provided' });
const { albumId } = req.body;
if (!albumId) return res.status(400).json({ message: 'No album provided' });
/*
Make sure the album exists
*/
const exists = await db
.table('albums')
.where({ id: albumId, userId: user.id })
.first();
if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
/*
Count the amount of links created for that album already and error out if max was reached
*/
const count = await db
.table('links')
.where('albumId', albumId)
.count({ count: 'id' })
.first();
if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' });
let { identifier } = req.body;
if (identifier) {
if (!user.isAdmin) return res.status(401).json({ message: 'Only administrators can create custom links' });
if (!(/^[a-zA-Z0-9-_]+$/.test(identifier))) return res.status(400).json({ message: 'Only alphanumeric, dashes, and underscore characters are allowed' });
/*
Make sure that the id doesn't already exists in the database
*/
const idExists = await db
.table('links')
.where({ identifier })
.first();
if (idExists) return res.status(400).json({ message: 'Album with this identifier already exists' });
} else {
/*
Try to allocate a new identifier in the database
*/
identifier = await Util.getUniqueAlbumIdentifier();
if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' });
}
try {
const insertObj = {
identifier,
userId: user.id,
albumId,
enabled: true,
enableDownload: true,
expiresAt: null,
views: 0
};
await db.table('links').insert(insertObj);
return res.json({
message: 'The link was created successfully',
data: insertObj
});
} catch (error) {
return super.error(res, error);
}
}
}
module.exports = linkPOST;

View File

@ -0,0 +1,22 @@
const Route = require('../../../structures/Route');
class linkPOST extends Route {
constructor() {
super('/album/:id/links', 'get');
}
async run(req, res, db, user) {
const { id } = req.params;
if (!id) return res.status(400).json({ message: 'Invalid id supplied' });
const links = await db.table('links')
.where({ albumId: id, userId: user.id });
return res.json({
message: 'Successfully retrieved links',
links
});
}
}
module.exports = linkPOST;

Some files were not shown because too many files have changed in this diff Show More