From 454825558aef4320c82b3d3148537038364c9427 Mon Sep 17 00:00:00 2001 From: 8cy <50817549+8cy@users.noreply.github.com> Date: Sat, 23 May 2020 02:24:18 -0700 Subject: DARLING in the FRANXX --- .gitattributes | 2 + .gitignore | 14 + Dockerfile | 19 + LICENSE | 21 + README.md | 71 +++ config.sample.js | 103 ++++ controllers/albumsController.js | 179 ++++++ controllers/authController.js | 91 +++ controllers/tokenController.js | 34 ++ controllers/uploadController.js | 311 +++++++++++ controllers/utilsController.js | 75 +++ database/db.js | 53 ++ database/migration.js | 13 + nginx-ssl.sample.conf | 56 ++ nginx.sample.conf | 45 ++ package.json | 41 ++ pages/album.html | 60 ++ pages/auth.html | 87 +++ pages/dashboard.html | 100 ++++ pages/error/404.html | 47 ++ pages/error/500.html | 47 ++ pages/faq.html | 83 +++ pages/home.html | 94 ++++ public/css/style.css | 121 ++++ public/images/backup/logo.png | Bin 0 -> 157334 bytes public/images/backup/logo_big.png | Bin 0 -> 4245986 bytes public/images/backup/logo_smol.png | Bin 0 -> 59261 bytes public/images/backup/logo_square.png | Bin 0 -> 139609 bytes public/images/fb_share.png | Bin 0 -> 63834 bytes public/images/icons/android-chrome-192x192.png | Bin 0 -> 40496 bytes public/images/icons/android-chrome-384x384.png | Bin 0 -> 155914 bytes public/images/icons/apple-touch-icon.png | Bin 0 -> 28610 bytes .../images/icons/backup/android-chrome-192x192.png | Bin 0 -> 11300 bytes .../images/icons/backup/android-chrome-384x384.png | Bin 0 -> 31742 bytes public/images/icons/backup/apple-touch-icon.png | Bin 0 -> 8070 bytes public/images/icons/backup/favicon-16x16.png | Bin 0 -> 920 bytes public/images/icons/backup/favicon-32x32.png | Bin 0 -> 1680 bytes public/images/icons/backup/favicon.ico | Bin 0 -> 15086 bytes public/images/icons/backup/mstile-150x150.png | Bin 0 -> 6268 bytes public/images/icons/backup/safari-pinned-tab.svg | 47 ++ public/images/icons/browserconfig.xml | 9 + public/images/icons/favicon-16x16.png | Bin 0 -> 1409 bytes public/images/icons/favicon-32x32.png | Bin 0 -> 2875 bytes public/images/icons/favicon.ico | Bin 0 -> 4286 bytes public/images/icons/manifest.json | 18 + public/images/logo.png | Bin 0 -> 178481 bytes public/images/logo_big.png | Bin 0 -> 4632784 bytes public/images/logo_smol.png | Bin 0 -> 43010 bytes public/images/logo_square.png | Bin 0 -> 113495 bytes public/js/auth.js | 56 ++ public/js/dashboard.js | 620 +++++++++++++++++++++ public/js/home.js | 226 ++++++++ public/json/quotes.json | 25 + real-ip-from-cf | 30 + routes/album.js | 56 ++ routes/api.js | 37 ++ strelizia.js | 58 ++ views/album.handlebars | 74 +++ 58 files changed, 3023 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.sample.js create mode 100644 controllers/albumsController.js create mode 100644 controllers/authController.js create mode 100644 controllers/tokenController.js create mode 100644 controllers/uploadController.js create mode 100644 controllers/utilsController.js create mode 100644 database/db.js create mode 100644 database/migration.js create mode 100644 nginx-ssl.sample.conf create mode 100644 nginx.sample.conf create mode 100644 package.json create mode 100644 pages/album.html create mode 100644 pages/auth.html create mode 100644 pages/dashboard.html create mode 100644 pages/error/404.html create mode 100644 pages/error/500.html create mode 100644 pages/faq.html create mode 100644 pages/home.html create mode 100644 public/css/style.css create mode 100644 public/images/backup/logo.png create mode 100644 public/images/backup/logo_big.png create mode 100644 public/images/backup/logo_smol.png create mode 100644 public/images/backup/logo_square.png create mode 100644 public/images/fb_share.png create mode 100644 public/images/icons/android-chrome-192x192.png create mode 100644 public/images/icons/android-chrome-384x384.png create mode 100644 public/images/icons/apple-touch-icon.png create mode 100644 public/images/icons/backup/android-chrome-192x192.png create mode 100644 public/images/icons/backup/android-chrome-384x384.png create mode 100644 public/images/icons/backup/apple-touch-icon.png create mode 100644 public/images/icons/backup/favicon-16x16.png create mode 100644 public/images/icons/backup/favicon-32x32.png create mode 100644 public/images/icons/backup/favicon.ico create mode 100644 public/images/icons/backup/mstile-150x150.png create mode 100644 public/images/icons/backup/safari-pinned-tab.svg create mode 100644 public/images/icons/browserconfig.xml create mode 100644 public/images/icons/favicon-16x16.png create mode 100644 public/images/icons/favicon-32x32.png create mode 100644 public/images/icons/favicon.ico create mode 100644 public/images/icons/manifest.json create mode 100644 public/images/logo.png create mode 100644 public/images/logo_big.png create mode 100644 public/images/logo_smol.png create mode 100644 public/images/logo_square.png create mode 100644 public/js/auth.js create mode 100644 public/js/dashboard.js create mode 100644 public/js/home.js create mode 100644 public/json/quotes.json create mode 100644 real-ip-from-cf create mode 100644 routes/album.js create mode 100644 routes/api.js create mode 100644 strelizia.js create mode 100644 views/album.handlebars diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c95e05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +!.gitkeep +node_modules/ +uploads/ +logs/ +database/db +config.js +start.json +npm-debug.log +pages/custom/** +migrate.js +yarn.lock +package-lock.json +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb87d7e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:9 + +LABEL name "strelizia" +LABEL version "3.0.0" +LABEL maintainer "iCrawl " + +WORKDIR /usr/src/strelizia + +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", "strelizia.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6895914 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa46b25 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +![lolisafe](https://lolisafe.moe/8KFePddY.png) +[![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/badge/Support-Buy%20me%20a%20coffee-yellow.svg?style=flat-square)](https://www.buymeacoffee.com/kana) + +# lolisafe, a small safe worth protecting. + +## 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 + +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. + +## 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 + +## 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`. + +Upon running the service for the first time, it's gonna create a user account with the username `root` and password `root`. This is your admin account and you should change the password immediately. This account will let you manage all uploaded files and remove any if necessary. + +The 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 + +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. 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) + +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) + +## Author + +**lolisafe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/lolisafe/blob/master/LICENSE) License.
+Authored and maintained by Pitu. + +> [lolisafe.moe](https://lolisafe.moe) · GitHub [@Pitu](https://github.com/Pitu) diff --git a/config.sample.js b/config.sample.js new file mode 100644 index 0000000..14cf77d --- /dev/null +++ b/config.sample.js @@ -0,0 +1,103 @@ +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 strelizia 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 strelizia 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://strelizia.cc/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.strelizia.cc/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://strelizia.cc', + + // 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 + } +} diff --git a/controllers/albumsController.js b/controllers/albumsController.js new file mode 100644 index 0000000..a395f9b --- /dev/null +++ b/controllers/albumsController.js @@ -0,0 +1,179 @@ +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; diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..eb7df09 --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,91 @@ +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; diff --git a/controllers/tokenController.js b/controllers/tokenController.js new file mode 100644 index 0000000..cbcc550 --- /dev/null +++ b/controllers/tokenController.js @@ -0,0 +1,34 @@ +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; diff --git a/controllers/uploadController.js b/controllers/uploadController.js new file mode 100644 index 0000000..d5615d0 --- /dev/null +++ b/controllers/uploadController.js @@ -0,0 +1,311 @@ +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; diff --git a/controllers/utilsController.js b/controllers/utilsController.js new file mode 100644 index 0000000..86c84b6 --- /dev/null +++ b/controllers/utilsController.js @@ -0,0 +1,75 @@ +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; diff --git a/database/db.js b/database/db.js new file mode 100644 index 0000000..8655f76 --- /dev/null +++ b/database/db.js @@ -0,0 +1,53 @@ +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; diff --git a/database/migration.js b/database/migration.js new file mode 100644 index 0000000..7e5b0e9 --- /dev/null +++ b/database/migration.js @@ -0,0 +1,13 @@ +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 strelizia normally'); +}; + +migration.start(); diff --git a/nginx-ssl.sample.conf b/nginx-ssl.sample.conf new file mode 100644 index 0000000..0d54fc6 --- /dev/null +++ b/nginx-ssl.sample.conf @@ -0,0 +1,56 @@ +upstream backend { + server 127.0.0.1:9999; # Change to the port you specified on strelizia +} + +map $sent_http_content_type $charset { + ~^text/ utf-8; +} + +server { + listen 80; + listen [::]:80; + server_name strelizia.cc; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name strelizia.cc; + 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 strelizia behind CloudFlare. + # This requires NGINX compiled from source with: + # --with-http_realip_module + #include /path/to/strelizia/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; + } +} diff --git a/nginx.sample.conf b/nginx.sample.conf new file mode 100644 index 0000000..07b69e5 --- /dev/null +++ b/nginx.sample.conf @@ -0,0 +1,45 @@ +upstream backend { + server 127.0.0.1:9999; # Change to the port you specified on strelizia +} + +map $sent_http_content_type $charset { + ~^text/ utf-8; +} + +server { + listen 80; + listen [::]:80; + + server_name strelizia.cc; + 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 strelizia behind CloudFlare. + # This requires NGINX compiled from source with: + # --with-http_realip_module + #include /path/to/strelizia/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; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8a681df --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "strelizia", + "version": "3.0.0", + "description": "Blazing fast file uploader and awesome bunker written in node! 🚀", + "author": "Sin", + "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.25.3", + "sqlite3": "^4.2.0" + }, + "devDependencies": { + "eslint": "^4.18.2", + "eslint-config-aqua": "^1.4.1" + }, + "eslintConfig": { + "extends": [ + "aqua" + ], + "env": { + "browser": true, + "node": true + }, + "rules": { + "func-names": 0 + } + } +} diff --git a/pages/album.html b/pages/album.html new file mode 100644 index 0000000..7714c26 --- /dev/null +++ b/pages/album.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strelizia - A small safe worth protecting. + + + + + + + + + + +
+
+
+

+

+
+
+
+
+
+ +
+
+
+ + + diff --git a/pages/auth.html b/pages/auth.html new file mode 100644 index 0000000..d87cc50 --- /dev/null +++ b/pages/auth.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strelizia - A small safe worth protecting. + + + + + + + + + + + + +
+
+
+

+ Dashboard Access +

+

+ Login or register +

+
+
+

+ +

+

+ +

+ +

+ + Register + + + Log in + +

+ +
+
+
+
+
+
+
+ + + diff --git a/pages/dashboard.html b/pages/dashboard.html new file mode 100644 index 0000000..a83eac1 --- /dev/null +++ b/pages/dashboard.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strelizia - A small safe worth protecting. + + + + + + + + + + +
+ +
+
+

+ Admin dashboard +

+

+

+ + Check +

+

+
+
+ +
+ +
+ +
+

Dashboard

+

A simple dashboard, to sort your uploaded stuff

+
+
+
+ +
+
+ +
+
+
+ +
+ + diff --git a/pages/error/404.html b/pages/error/404.html new file mode 100644 index 0000000..6f770a7 --- /dev/null +++ b/pages/error/404.html @@ -0,0 +1,47 @@ + + + + strelizia + + + + + + +
+
+
Page not found.
+
+
+ + diff --git a/pages/error/500.html b/pages/error/500.html new file mode 100644 index 0000000..749e95a --- /dev/null +++ b/pages/error/500.html @@ -0,0 +1,47 @@ + + + + strelizia + + + + + + +
+
+
Internal server error.
+
+
+ + diff --git a/pages/faq.html b/pages/faq.html new file mode 100644 index 0000000..13b648d --- /dev/null +++ b/pages/faq.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strelizia - A small safe worth protecting. + + + + + +
+
+
+ +

What is strelizia?

+
+
+ strelizia 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. +
+
+ +

Will you keep my files forever?

+
+
+ Unless we receive a copyright complain or some other bullshit, we will. +
+
+ +

How can I keep track of my uploads?

+
+
+ 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. +
+
+ + + +

Why should I use this?

+
+
+ 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, strelizia 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. +
+
+ +
+
+
+ + + diff --git a/pages/home.html b/pages/home.html new file mode 100644 index 0000000..841e320 --- /dev/null +++ b/pages/home.html @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + strelizia - A small safe worth protecting. + + + + + + + + + + + +
+
+
+

+ +

+

strelizia

+

+ +

+
+
+
+ Running in private mode. Log in to upload. + +
+
+
+ +
+
+
+
+ +

+ +
+
+
+
+ +

+ + +
+
+
+ + + diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..993811e --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,121 @@ +section#home { + user-select: none; +} + +section#home .link { + user-select: all; +} + +/* ------------------ + 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; +} diff --git a/public/images/backup/logo.png b/public/images/backup/logo.png new file mode 100644 index 0000000..96916c0 Binary files /dev/null and b/public/images/backup/logo.png differ diff --git a/public/images/backup/logo_big.png b/public/images/backup/logo_big.png new file mode 100644 index 0000000..b935a28 Binary files /dev/null and b/public/images/backup/logo_big.png differ diff --git a/public/images/backup/logo_smol.png b/public/images/backup/logo_smol.png new file mode 100644 index 0000000..94b6797 Binary files /dev/null and b/public/images/backup/logo_smol.png differ diff --git a/public/images/backup/logo_square.png b/public/images/backup/logo_square.png new file mode 100644 index 0000000..0c296b1 Binary files /dev/null and b/public/images/backup/logo_square.png differ diff --git a/public/images/fb_share.png b/public/images/fb_share.png new file mode 100644 index 0000000..d5fc25b Binary files /dev/null and b/public/images/fb_share.png differ diff --git a/public/images/icons/android-chrome-192x192.png b/public/images/icons/android-chrome-192x192.png new file mode 100644 index 0000000..316c49a Binary files /dev/null and b/public/images/icons/android-chrome-192x192.png differ diff --git a/public/images/icons/android-chrome-384x384.png b/public/images/icons/android-chrome-384x384.png new file mode 100644 index 0000000..d513acc Binary files /dev/null and b/public/images/icons/android-chrome-384x384.png differ diff --git a/public/images/icons/apple-touch-icon.png b/public/images/icons/apple-touch-icon.png new file mode 100644 index 0000000..080f791 Binary files /dev/null and b/public/images/icons/apple-touch-icon.png differ diff --git a/public/images/icons/backup/android-chrome-192x192.png b/public/images/icons/backup/android-chrome-192x192.png new file mode 100644 index 0000000..1e77f89 Binary files /dev/null and b/public/images/icons/backup/android-chrome-192x192.png differ diff --git a/public/images/icons/backup/android-chrome-384x384.png b/public/images/icons/backup/android-chrome-384x384.png new file mode 100644 index 0000000..c07f93c Binary files /dev/null and b/public/images/icons/backup/android-chrome-384x384.png differ diff --git a/public/images/icons/backup/apple-touch-icon.png b/public/images/icons/backup/apple-touch-icon.png new file mode 100644 index 0000000..3590469 Binary files /dev/null and b/public/images/icons/backup/apple-touch-icon.png differ diff --git a/public/images/icons/backup/favicon-16x16.png b/public/images/icons/backup/favicon-16x16.png new file mode 100644 index 0000000..bf15749 Binary files /dev/null and b/public/images/icons/backup/favicon-16x16.png differ diff --git a/public/images/icons/backup/favicon-32x32.png b/public/images/icons/backup/favicon-32x32.png new file mode 100644 index 0000000..e213772 Binary files /dev/null and b/public/images/icons/backup/favicon-32x32.png differ diff --git a/public/images/icons/backup/favicon.ico b/public/images/icons/backup/favicon.ico new file mode 100644 index 0000000..f18ab34 Binary files /dev/null and b/public/images/icons/backup/favicon.ico differ diff --git a/public/images/icons/backup/mstile-150x150.png b/public/images/icons/backup/mstile-150x150.png new file mode 100644 index 0000000..a84a7e4 Binary files /dev/null and b/public/images/icons/backup/mstile-150x150.png differ diff --git a/public/images/icons/backup/safari-pinned-tab.svg b/public/images/icons/backup/safari-pinned-tab.svg new file mode 100644 index 0000000..5c1c698 --- /dev/null +++ b/public/images/icons/backup/safari-pinned-tab.svg @@ -0,0 +1,47 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/public/images/icons/browserconfig.xml b/public/images/icons/browserconfig.xml new file mode 100644 index 0000000..f95ebd2 --- /dev/null +++ b/public/images/icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #00aba9 + + + diff --git a/public/images/icons/favicon-16x16.png b/public/images/icons/favicon-16x16.png new file mode 100644 index 0000000..3ffa533 Binary files /dev/null and b/public/images/icons/favicon-16x16.png differ diff --git a/public/images/icons/favicon-32x32.png b/public/images/icons/favicon-32x32.png new file mode 100644 index 0000000..6896449 Binary files /dev/null and b/public/images/icons/favicon-32x32.png differ diff --git a/public/images/icons/favicon.ico b/public/images/icons/favicon.ico new file mode 100644 index 0000000..39ac387 Binary files /dev/null and b/public/images/icons/favicon.ico differ diff --git a/public/images/icons/manifest.json b/public/images/icons/manifest.json new file mode 100644 index 0000000..c5e008f --- /dev/null +++ b/public/images/icons/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "strelizia", + "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" +} \ No newline at end of file diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..124ca01 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/logo_big.png b/public/images/logo_big.png new file mode 100644 index 0000000..ad81912 Binary files /dev/null and b/public/images/logo_big.png differ diff --git a/public/images/logo_smol.png b/public/images/logo_smol.png new file mode 100644 index 0000000..0ca96d2 Binary files /dev/null and b/public/images/logo_smol.png differ diff --git a/public/images/logo_square.png b/public/images/logo_square.png new file mode 100644 index 0000000..cd840d1 Binary files /dev/null and b/public/images/logo_square.png differ diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..1ae9e4d --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,56 @@ +var page = {}; + +page.do = function(dest){ + + var user = document.getElementById('user').value; + var pass = document.getElementById('pass').value; + + if(user === undefined || user === null || user === '') + return swal('Error', 'You need to specify a username', 'error'); + if(pass === undefined || pass === null || pass === '') + return swal('Error', 'You need to specify a username', 'error'); + + axios.post('/api/' + dest, { + username: user, + password: pass + }) + .then(function (response) { + + if(response.data.success === false) + return swal('Error', response.data.description, 'error'); + + localStorage.token = response.data.token; + window.location = '/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(); +}; \ No newline at end of file diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..b080402 --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,620 @@ +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 = ``; + var listType = ` +
+ +
`; + + if(panel.filesView === 'thumbs'){ + + container.innerHTML = ` + ${pagination} +
+ ${listType} +
+ +
+ ${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 = ``; + else + div.innerHTML = `

.${item.file.split('.').pop()}

`; + table.appendChild(div); + + } + + }else{ + + var albumOrUser = 'Album'; + if(panel.username === 'root') + albumOrUser = 'User'; + + container.innerHTML = ` + ${pagination} +
+ ${listType} + + + + + + + + + + + +
File${albumOrUser}Date
+
+ ${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 = ` + + ${item.file} + ${displayAlbumOrUser} + ${item.date} + + + + + + + + + `; + + 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 = ` +

Create new album

+ +

+ + Submit +

+ +

List of albums

+ + + + + + + + + + + + + +
NameFilesCreated AtPublic link
`; + + panel.page.appendChild(container); + var table = document.getElementById('table'); + + for(var item of response.data.albums){ + + var tr = document.createElement('tr'); + tr.innerHTML = ` + + ${item.name} + ${item.files} + ${item.date} + Album link + + + + + + + + + + + + + + `; + + 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 = ` +

Manage your token

+ + +

+ + Request new token +

+ `; + + 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 = ` +

Change your password

+ + +

+ +

+ +

+ + Set new password +

+ `; + + 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(); +}; diff --git a/public/js/home.js b/public/js/home.js new file mode 100644 index 0000000..0f5b1ee --- /dev/null +++ b/public/js/home.js @@ -0,0 +1,226 @@ +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'; + + $.getJSON("../json/quotes.json", (data) => { + var items = []; + let quoteNum = Math.floor(Math.random() * data.length); + document.getElementById('quotes').innerHTML = data[quoteNum]; + }); + + document.getElementById('maxFileSize').innerHTML = 'Maximum upload size per file is 1024TB' /*+ 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(); +}; + diff --git a/public/json/quotes.json b/public/json/quotes.json new file mode 100644 index 0000000..9c82bca --- /dev/null +++ b/public/json/quotes.json @@ -0,0 +1,25 @@ +[ + "gang shit bitch", + "sin is a loser", + "why are u even on here???", + "gang gang", + "uwu", + "gang shit darling", + "gang gang darling", + "wtf", + "its one of you slipsteam kids reading this isn't it?", + "zero two gang uwu", + "fbi ain't got shit on this", + "if u the fbi please go away", + "~uwu", + "fbi pwease go away ~uwu", + "please don't tell me its one of you slipstream kids", + "u want an invite don't you", + "whats up normie", + "hello no name", + "whats up no name", + "oh look, its another no namer", + "hi newfag", + "why u snoopin", + "you will never amount to anything" +] \ No newline at end of file diff --git a/real-ip-from-cf b/real-ip-from-cf new file mode 100644 index 0000000..e39c1d2 --- /dev/null +++ b/real-ip-from-cf @@ -0,0 +1,30 @@ +# 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; diff --git a/routes/album.js b/routes/album.js new file mode 100644 index 0000000..46f00d3 --- /dev/null +++ b/routes/album.js @@ -0,0 +1,56 @@ +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 = ``; + } else { + file.thumb = `

.${ext}

`; + } + } + + + 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; diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..5a4d355 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,37 @@ +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; diff --git a/strelizia.js b/strelizia.js new file mode 100644 index 0000000..4e4dfc2 --- /dev/null +++ b/strelizia.js @@ -0,0 +1,58 @@ +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(`strelizia started on port ${config.port}`)); diff --git a/views/album.handlebars b/views/album.handlebars new file mode 100644 index 0000000..c959c43 --- /dev/null +++ b/views/album.handlebars @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ title }} + + + + + + + + + +
+
+
+
+
+

{{ title }}

+

{{ count }} files

+
+
+ {{#if enableDownload}} + Download Album + {{/if}} +
+
+
+
+
+
+
+
+ {{#each files}} + + {{/each}} +
+
+
+
+ + + -- cgit v1.2.3