Browse Source

Updates (very important to read)

Client-side CSS & JS files will now be processed with Gulp.
Gulp tasks are configured in gulpfile.js file.

CSS files will be optimized with postcss-preset-env, which will
auto-add vendor prefixes and convert any parts necessary for browsers
compatibility.
Afterwards they will be minified with cssnano.

JS files will be optimized with bublé,
likewise for browsers compatibility.
Afterwards they will be minified with terser.

Unprocessed CSS & JS files will now be located at src directory, while
the processed results will be located at dist directory.

Due to bublé, the JS files should now be compatible up to IE 11
at the minimum.
Previously the safe would not work in IE 11 due to extensive usage of
template literals.
Due to that as well, JS files in src directory will now extensively use
arrow functions for my personal comfort (as they will be converted too).

The server will use the processed files at dist directory by default.
If you want to rebuild the files by your own, you can run "yarn build".
Gulp is a development dependency, so make sure you have installed all
development dependencies (e.i. NOT using "yarn install --production").

---

yarn lint -> gulp lint

yarn build -> gulp default

yarn watch -> gulp watch

yarn develop -> env NODE_ENV=development yarn watch

---

Fixed not being able to demote staff into normal users.

/api/token/verify will no longer respond with 401 HTTP error code,
unless an error occurred (which will be 500 HTTP error code).

Fixed /nojs route not displaying file's original name when a duplicate
is found on the server.

Removed is-breeze CSS class name, in favor of Bulma's is-info.

Removed custom styling from auth page, in favor of global styling.

Removed all usage of style HTML attribute in favor of CSS classes.

Renamed js/s/ to js/misc/.

Use loading spinners on dashboard's sidebar menus.

Disable all other sidebar menus when something is loading.

Changed title HTML attribute of disabled control buttons in
uploads & users list.

Hid checkboxes and WIP controls from users list.

Better error messages handling.
Especially homepage will now support CF's HTTP error codes.

Updated various icons.
Also, added fontello config file at public/libs/fontello/config.json.
This should let you edit them more easily with fontello.

Use Gatsby icon for my blog's link in homepage's footer.

A bunch of other improvements here & there.
Bobby Wibowo 1 month ago
parent
commit
c9ba16e1d6
78 changed files with 4123 additions and 986 deletions
  1. 7 0
      .browserslistrc
  2. 2 0
      .eslintignore
  3. 60 14
      .gitignore
  4. 2 0
      .stylelintignore
  5. 0 0
      .stylelintrc.json
  6. 5 5
      controllers/albumsController.js
  7. 5 5
      controllers/authController.js
  8. 22 26
      controllers/pathsController.js
  9. 6 6
      controllers/tokenController.js
  10. 11 6
      controllers/uploadController.js
  11. 17 5
      controllers/utilsController.js
  12. 2 2
      database/db.js
  13. 1 1
      database/migration.js
  14. 2 0
      dist/css/album.css
  15. 1 0
      dist/css/album.css.map
  16. 2 0
      dist/css/dashboard.css
  17. 1 0
      dist/css/dashboard.css.map
  18. 2 0
      dist/css/home.css
  19. 1 0
      dist/css/home.css.map
  20. 2 0
      dist/css/style.css
  21. 1 0
      dist/css/style.css.map
  22. 2 0
      dist/css/sweetalert.css
  23. 1 0
      dist/css/sweetalert.css.map
  24. 2 0
      dist/css/thumbs.css
  25. 1 0
      dist/css/thumbs.css.map
  26. 2 0
      dist/js/album.js
  27. 1 0
      dist/js/album.js.map
  28. 2 0
      dist/js/auth.js
  29. 1 0
      dist/js/auth.js.map
  30. 2 0
      dist/js/dashboard.js
  31. 1 0
      dist/js/dashboard.js.map
  32. 2 0
      dist/js/home.js
  33. 1 0
      dist/js/home.js.map
  34. 2 0
      dist/js/misc/render.js
  35. 1 0
      dist/js/misc/render.js.map
  36. 2 0
      dist/js/misc/utils.js
  37. 1 0
      dist/js/misc/utils.js.map
  38. 106 0
      gulpfile.js
  39. 18 17
      lolisafe.js
  40. 21 3
      package.json
  41. 0 31
      public/css/auth.css
  42. 0 9
      public/libs/fontello/LICENSE
  43. 240 0
      public/libs/fontello/config.json
  44. 27 26
      public/libs/fontello/fontello.css
  45. BIN
      public/libs/fontello/fontello.eot
  46. 24 22
      public/libs/fontello/fontello.svg
  47. BIN
      public/libs/fontello/fontello.ttf
  48. BIN
      public/libs/fontello/fontello.woff
  49. BIN
      public/libs/fontello/fontello.woff2
  50. 3 3
      routes/album.js
  51. 3 3
      routes/api.js
  52. 1 1
      routes/nojs.js
  53. 0 13
      scripts/_utils.js
  54. 1 2
      scripts/cf-purge.js
  55. 4 4
      scripts/clean-up.js
  56. 2 3
      scripts/delete-expired.js
  57. 3 4
      scripts/thumbs.js
  58. 0 0
      src/css/album.css
  59. 13 14
      public/css/dashboard.css
  60. 2 31
      public/css/home.css
  61. 20 31
      public/css/style.css
  62. 0 9
      public/css/sweetalert.css
  63. 0 14
      public/css/thumbs.css
  64. 4 2
      public/js/.eslintrc.json
  65. 1 1
      public/js/album.js
  66. 17 13
      public/js/auth.js
  67. 453 357
      public/js/dashboard.js
  68. 180 149
      public/js/home.js
  69. 6 6
      public/js/s/render.js
  70. 5 5
      public/js/s/utils.js
  71. 5 4
      todo.md
  72. 6 6
      views/_globals.njk
  73. 2 2
      views/album.njk
  74. 1 2
      views/auth.njk
  75. 16 16
      views/dashboard.njk
  76. 13 9
      views/home.njk
  77. 13 7
      views/nojs.njk
  78. 2737 97
      yarn.lock

+ 7 - 0
.browserslistrc

@@ -0,0 +1,7 @@
+# Browserslist's defaults (supports IE 11 at the minimum)
+# https://github.com/browserslist/browserslist#queries
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead

+ 2 - 0
.eslintignore

@@ -1 +1,3 @@
 **/*.min.js
+dist/js/*
+public/libs/*

+ 60 - 14
.gitignore

@@ -1,17 +1,63 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Dependency directories
+node_modules/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# npm's package-lock (if npm is accidentally used)
+package-lock.json
+
+# vscode's workspace settings
+/.vscode
+
+# Configuration file
+/config.js
+
+# Database
+/database/db
+
+# Uploads directory
+/uploads
+
+# Custom pages directory
+/pages/custom
+
+# User files
 .DS_Store
 .nvmrc
-.vscode
 !.gitkeep
-node_modules
-uploads
-logs
-database/db
-config.js
-start.json
-npm-debug.log
-pages/custom/**
-migrate.js
-yarn-error.log
-package-lock.json
-public/render/**/original
-public/render/**/wip
+/start.json
+/migrate.js
+
+# User directories (renders)
+/public/render/**/original
+/public/render/**/wip

+ 2 - 0
.stylelintignore

@@ -0,0 +1,2 @@
+dist/css/*
+public/libs/*

.stylelintrc → .stylelintrc.json


+ 5 - 5
controllers/albumsController.js

@@ -1,13 +1,13 @@
-const config = require('./../config')
-const db = require('knex')(config.database)
 const EventEmitter = require('events')
 const fs = require('fs')
-const logger = require('./../logger')
 const path = require('path')
-const paths = require('./pathsController')
 const randomstring = require('randomstring')
-const utils = require('./utilsController')
 const Zip = require('jszip')
+const paths = require('./pathsController')
+const utils = require('./utilsController')
+const config = require('./../config')
+const logger = require('./../logger')
+const db = require('knex')(config.database)
 
 const self = {
   onHold: new Set()

+ 5 - 5
controllers/authController.js

@@ -1,12 +1,12 @@
 const { promisify } = require('util')
 const bcrypt = require('bcrypt')
-const config = require('./../config')
-const db = require('knex')(config.database)
-const logger = require('./../logger')
-const perms = require('./permissionController')
 const randomstring = require('randomstring')
+const perms = require('./permissionController')
 const tokens = require('./tokenController')
 const utils = require('./utilsController')
+const config = require('./../config')
+const logger = require('./../logger')
+const db = require('knex')(config.database)
 
 const self = {
   compare: promisify(bcrypt.compare),
@@ -152,7 +152,7 @@ self.editUser = async (req, res, next) => {
       update.enabled = Boolean(req.body.enabled)
 
     if (req.body.group !== undefined) {
-      update.permission = perms.permissions[req.body.group] || target.permission
+      update.permission = perms.permissions[req.body.group]
       if (typeof update.permission !== 'number' || update.permission < 0)
         update.permission = target.permission
     }

+ 22 - 26
controllers/pathsController.js

@@ -1,8 +1,8 @@
 const { promisify } = require('util')
-const config = require('./../config')
 const fs = require('fs')
-const logger = require('./../logger')
 const path = require('path')
+const config = require('./../config')
+const logger = require('./../logger')
 
 const self = {}
 
@@ -33,6 +33,7 @@ self.thumbPlaceholder = path.resolve(config.uploads.generateThumbs.placeholder |
 self.logs = path.resolve(config.logsFolder)
 
 self.customPages = path.resolve('pages/custom')
+self.dist = path.resolve('dist')
 self.public = path.resolve('public')
 
 self.errorRoot = path.resolve(config.errorPages.rootDir)
@@ -47,33 +48,28 @@ const verify = [
 ]
 
 self.init = async () => {
-  try {
-    for (const p of verify)
-      try {
-        await self.access(p)
-      } catch (err) {
-        if (err.code !== 'ENOENT') {
-          logger.error(err)
-        } else {
-          const mkdir = await self.mkdir(p)
-          if (mkdir)
-            logger.log(`Created directory: ${p}`)
-        }
+  // Check & create directories
+  for (const p of verify)
+    try {
+      await self.access(p)
+    } catch (err) {
+      if (err.code !== 'ENOENT') {
+        throw err
+      } else {
+        const mkdir = await self.mkdir(p)
+        if (mkdir)
+          logger.log(`Created directory: ${p}`)
       }
-
-    // Purge chunks directory
-    const uuidDirs = await self.readdir(self.chunks)
-    for (const uuid of uuidDirs) {
-      const root = path.join(self.chunks, uuid)
-      const chunks = await self.readdir(root)
-      for (const chunk of chunks)
-        await self.unlink(path.join(root, chunk))
-      await self.rmdir(root)
     }
 
-    self.verified = true
-  } catch (error) {
-    logger.error(error)
+  // Purge any leftover in chunks directory
+  const uuidDirs = await self.readdir(self.chunks)
+  for (const uuid of uuidDirs) {
+    const root = path.join(self.chunks, uuid)
+    const chunks = await self.readdir(root)
+    for (const chunk of chunks)
+      await self.unlink(path.join(root, chunk))
+    await self.rmdir(root)
   }
 }
 

+ 6 - 6
controllers/tokenController.js

@@ -1,9 +1,9 @@
-const config = require('./../config')
-const db = require('knex')(config.database)
-const logger = require('./../logger')
-const perms = require('./permissionController')
 const randomstring = require('randomstring')
+const perms = require('./permissionController')
 const utils = require('./utilsController')
+const config = require('./../config')
+const logger = require('./../logger')
+const db = require('knex')(config.database)
 
 const self = {
   tokenLength: 64,
@@ -41,7 +41,7 @@ self.verify = async (req, res, next) => {
     : ''
 
   if (!token)
-    return res.status(401).json({ success: false, description: 'No token provided.' })
+    return res.json({ success: false, description: 'No token provided.' })
 
   try {
     const user = await db.table('users')
@@ -50,7 +50,7 @@ self.verify = async (req, res, next) => {
       .first()
 
     if (!user)
-      return res.status(401).json({ success: false, description: 'Invalid token.' })
+      return res.json({ success: false, description: 'Invalid token.' })
 
     return res.json({
       success: true,

+ 11 - 6
controllers/uploadController.js

@@ -1,15 +1,15 @@
-const config = require('./../config')
 const crypto = require('crypto')
-const db = require('knex')(config.database)
 const fetch = require('node-fetch')
 const fs = require('fs')
-const logger = require('./../logger')
 const multer = require('multer')
 const path = require('path')
+const randomstring = require('randomstring')
 const paths = require('./pathsController')
 const perms = require('./permissionController')
-const randomstring = require('randomstring')
 const utils = require('./utilsController')
+const config = require('./../config')
+const logger = require('./../logger')
+const db = require('knex')(config.database)
 
 const self = {}
 
@@ -563,6 +563,11 @@ self.storeFilesToDb = async (req, res, user, infoMap) => {
       // Continue even when encountering errors
       await utils.unlinkFile(info.data.filename).catch(logger.error)
       // logger.log(`Unlinked ${info.data.filename} since a duplicate named ${dbFile.name} exists`)
+
+      // If on /nojs route, append original file name reported by client
+      if (req.path === '/nojs')
+        dbFile.original = info.data.originalname
+
       exists.push(dbFile)
       continue
     }
@@ -635,11 +640,11 @@ self.sendUploadResponse = async (req, res, result) => {
         url: `${config.domain}/${file.name}`
       }
 
-      // Add expiry date if a temporary upload
+      // If a temporary upload, add expiry date
       if (file.expirydate)
         map.expirydate = file.expirydate
 
-      // Add original name if on /nojs route
+      // If on /nojs route, add original name
       if (req.path === '/nojs')
         map.original = file.original
 

+ 17 - 5
controllers/utilsController.js

@@ -1,15 +1,15 @@
 const { promisify } = require('util')
 const { spawn } = require('child_process')
-const config = require('./../config')
-const db = require('knex')(config.database)
 const fetch = require('node-fetch')
 const ffmpeg = require('fluent-ffmpeg')
-const logger = require('./../logger')
 const path = require('path')
-const paths = require('./pathsController')
-const perms = require('./permissionController')
 const sharp = require('sharp')
 const si = require('systeminformation')
+const paths = require('./pathsController')
+const perms = require('./permissionController')
+const config = require('./../config')
+const logger = require('./../logger')
+const db = require('knex')(config.database)
 
 const self = {
   clamd: {
@@ -148,6 +148,18 @@ self.escape = (string) => {
     : html
 }
 
+self.stripIndents = string => {
+  if (!string) return
+  const result = string.replace(/^[^\S\n]+/gm, '')
+  const match = result.match(/^[^\S\n]*(?=\S)/gm)
+  const indent = match && Math.min(...match.map(el => el.length))
+  if (indent) {
+    const regexp = new RegExp(`^.{${indent}}`, 'gm')
+    return result.replace(regexp, '')
+  }
+  return result
+}
+
 self.authorize = async (req, res) => {
   // TODO: Improve usage of this function by the other APIs
   const token = req.headers.token

+ 2 - 2
database/db.js

@@ -1,6 +1,6 @@
-const logger = require('./../logger')
-const perms = require('./../controllers/permissionController')
 const randomstring = require('randomstring')
+const perms = require('./../controllers/permissionController')
+const logger = require('./../logger')
 
 // TODO: Auto-detect missing columns here
 // That way we will no longer need the migration script

+ 1 - 1
database/migration.js

@@ -1,6 +1,6 @@
+const perms = require('./../controllers/permissionController')
 const config = require('./../config')
 const db = require('knex')(config.database)
-const perms = require('./../controllers/permissionController')
 
 const map = {
   files: {

+ 2 - 0
dist/css/album.css

@@ -0,0 +1,2 @@
+.section{background:none}@media screen and (max-width:768px){.description{text-align:center}}
+/*# sourceMappingURL=album.css.map */

+ 1 - 0
dist/css/album.css.map

@@ -0,0 +1 @@
+{"version":3,"sources":["album.css"],"names":[],"mappings":"AAAA,SACE,eACF,CAEA,oCACE,aACE,iBACF,CACF","file":"album.css","sourcesContent":[".section {\n  background: none\n}\n\[email protected] screen and (max-width: 768px) {\n  .description {\n    text-align: center\n  }\n}\n"]}

File diff suppressed because it is too large
+ 2 - 0
dist/css/dashboard.css


File diff suppressed because it is too large
+ 1 - 0
dist/css/dashboard.css.map


File diff suppressed because it is too large
+ 2 - 0
dist/css/home.css


File diff suppressed because it is too large
+ 1 - 0
dist/css/home.css.map


File diff suppressed because it is too large
+ 2 - 0
dist/css/style.css


File diff suppressed because it is too large
+ 1 - 0
dist/css/style.css.map


File diff suppressed because it is too large
+ 2 - 0
dist/css/sweetalert.css


File diff suppressed because it is too large
+ 1 - 0
dist/css/sweetalert.css.map


File diff suppressed because it is too large
+ 2 - 0
dist/css/thumbs.css


File diff suppressed because it is too large
+ 1 - 0
dist/css/thumbs.css.map


+ 2 - 0
dist/js/album.js

@@ -0,0 +1,2 @@
+var lsKeys={},page={lazyLoad:null};window.onload=function(){for(var e=document.querySelectorAll(".file-size"),a=0;a<e.length;a++)e[a].innerHTML=page.getPrettyBytes(parseInt(e[a].innerHTML.replace(/\s*B$/i,"")));page.lazyLoad=new LazyLoad};
+//# sourceMappingURL=album.js.map

File diff suppressed because it is too large
+ 1 - 0
dist/js/album.js.map


File diff suppressed because it is too large
+ 2 - 0
dist/js/auth.js


File diff suppressed because it is too large
+ 1 - 0
dist/js/auth.js.map


File diff suppressed because it is too large
+ 2 - 0
dist/js/dashboard.js


File diff suppressed because it is too large
+ 1 - 0
dist/js/dashboard.js.map


File diff suppressed because it is too large
+ 2 - 0
dist/js/home.js


File diff suppressed because it is too large
+ 1 - 0
dist/js/home.js.map


File diff suppressed because it is too large
+ 2 - 0
dist/js/misc/render.js


File diff suppressed because it is too large
+ 1 - 0
dist/js/misc/render.js.map


File diff suppressed because it is too large
+ 2 - 0
dist/js/misc/utils.js


File diff suppressed because it is too large
+ 1 - 0
dist/js/misc/utils.js.map


+ 106 - 0
gulpfile.js

@@ -0,0 +1,106 @@
+const gulp = require('gulp')
+const cssnano = require('cssnano')
+const del = require('del')
+const buble = require('gulp-buble')
+const eslint = require('gulp-eslint')
+const gulpif = require('gulp-if')
+const nodemon = require('gulp-nodemon')
+const postcss = require('gulp-postcss')
+const postcssPresetEnv = require('postcss-preset-env')
+const sourcemaps = require('gulp-sourcemaps')
+const stylelint = require('gulp-stylelint')
+const terser = require('gulp-terser')
+
+/** TASKS: LINT */
+
+gulp.task('lint:js', () => {
+  return gulp.src('./src/js/**/*.js')
+    .pipe(eslint())
+    .pipe(eslint.failAfterError())
+})
+
+gulp.task('lint:css', () => {
+  return gulp.src('./src/css/**/*.css')
+    .pipe(stylelint())
+})
+
+gulp.task('lint', gulp.parallel('lint:js', 'lint:css'))
+
+/** TASKS: CLEAN */
+
+gulp.task('clean:css', () => {
+  return del(['dist/css'])
+})
+
+gulp.task('clean:js', () => {
+  return del(['dist/js'])
+})
+
+gulp.task('clean', gulp.parallel('clean:css', 'clean:js'))
+
+/** TASKS: BUILD */
+
+gulp.task('build:css', () => {
+  const plugins = [
+    postcssPresetEnv()
+  ]
+
+  // Minify on production
+  if (process.env.NODE_ENV !== 'development')
+    plugins.push(cssnano())
+
+  return gulp.src('./src/css/**/*.css')
+    .pipe(sourcemaps.init())
+    .pipe(postcss(plugins))
+    .pipe(sourcemaps.write('.'))
+    .pipe(gulp.dest('./dist/css'))
+})
+
+gulp.task('build:js', () => {
+  return gulp.src('./src/js/**/*.js')
+    .pipe(sourcemaps.init())
+    .pipe(buble())
+    // Minify on production
+    .pipe(gulpif(process.env.NODE_ENV !== 'development', terser()))
+    .pipe(sourcemaps.write('.'))
+    .pipe(gulp.dest('./dist/js'))
+})
+
+gulp.task('build', gulp.parallel('build:css', 'build:js'))
+
+gulp.task('default', gulp.series('lint', 'clean', 'build'))
+
+/** TASKS: WATCH (SKIP LINTER) */
+
+gulp.task('watch:css', () => {
+  return gulp.watch([
+    'src/**/*.css'
+  ], gulp.series('clean:css', 'build:css'))
+})
+
+gulp.task('watch:js', () => {
+  return gulp.watch([
+    'src/**/*.js'
+  ], gulp.series('clean:js', 'build:js'))
+})
+
+gulp.task('watch:src', gulp.parallel('watch:css', 'watch:js'))
+
+gulp.task('nodemon', done => {
+  return nodemon({
+    script: './lolisafe.js',
+    env: process.env,
+    watch: [
+      'lolisafe.js',
+      'logger.js',
+      'config.js',
+      'controllers/',
+      'database/',
+      'routes/'
+    ],
+    ext: 'js',
+    done
+  })
+})
+
+gulp.task('watch', gulp.series('clean', 'build', gulp.parallel('watch:src', 'nodemon')))

+ 18 - 17
lolisafe.js

@@ -1,13 +1,13 @@
 const bodyParser = require('body-parser')
 const clamd = require('clamdjs')
-const config = require('./config')
 const express = require('express')
 const helmet = require('helmet')
-const logger = require('./logger')
 const nunjucks = require('nunjucks')
 const path = require('path')
 const RateLimit = require('express-rate-limit')
 const readline = require('readline')
+const config = require('./config')
+const logger = require('./logger')
 const safe = express()
 
 process.on('uncaughtException', error => {
@@ -50,6 +50,8 @@ if (Array.isArray(config.rateLimits) && config.rateLimits.length)
 safe.use(bodyParser.urlencoded({ extended: true }))
 safe.use(bodyParser.json())
 
+let setHeaders
+
 // Cache control (safe.fiery.me)
 if (config.cacheControl) {
   const cacheControls = {
@@ -66,16 +68,6 @@ if (config.cacheControl) {
     next()
   })
 
-  const setHeaders = res => {
-    res.set('Access-Control-Allow-Origin', '*')
-    res.set('Cache-Control', cacheControls.default)
-  }
-
-  if (config.serveFilesWithNode)
-    safe.use('/', express.static(paths.uploads, { setHeaders }))
-
-  safe.use('/', express.static(paths.public, { setHeaders }))
-
   // Do NOT cache these dynamic routes
   safe.use(['/a', '/api', '/nojs'], (req, res, next) => {
     res.set('Cache-Control', cacheControls.disable)
@@ -93,13 +85,19 @@ if (config.cacheControl) {
     setHeaders(res)
     next()
   })
-} else {
-  if (config.serveFilesWithNode)
-    safe.use('/', express.static(paths.uploads))
 
-  safe.use('/', express.static(paths.public))
+  setHeaders = res => {
+    res.set('Access-Control-Allow-Origin', '*')
+    res.set('Cache-Control', cacheControls.default)
+  }
 }
 
+if (config.serveFilesWithNode)
+  safe.use('/', express.static(paths.uploads, { setHeaders }))
+
+safe.use('/', express.static(paths.public, { setHeaders }))
+safe.use('/', express.static(paths.dist, { setHeaders }))
+
 safe.use('/', album)
 safe.use('/', nojs)
 safe.use('/api', api)
@@ -247,7 +245,10 @@ safe.use('/api', api)
         prompt: ''
       }).on('line', line => {
         try {
-          if (line === '.exit') process.exit(0)
+          if (line === 'rs')
+            return
+          if (line === '.exit')
+            return process.exit(0)
           // eslint-disable-next-line no-eval
           logger.log(eval(line))
         } catch (error) {

+ 21 - 3
package.json

@@ -15,8 +15,11 @@
   "license": "MIT",
   "scripts": {
     "start": "node ./lolisafe.js",
-    "startdev": "env NODE_ENV=development node ./lolisafe.js",
-    "pm2": "pm2 start --name safe ./lolisafe.js",
+    "pm2": "pm2 start ./lolisafe.js",
+    "lint": "gulp lint",
+    "build": "gulp default",
+    "watch": "gulp watch",
+    "develop": "env NODE_ENV=development yarn watch",
     "cf-purge": "node ./scripts/cf-purge.js",
     "clean-up": "node ./scripts/clean-up.js",
     "delete-expired": "node ./scripts/delete-expired.js",
@@ -43,12 +46,27 @@
     "systeminformation": "^4.14.8"
   },
   "devDependencies": {
-    "eslint": "^6.3.0",
+    "browserslist": "^4.7.0",
+    "cssnano": "^4.1.10",
+    "del": "^5.1.0",
+    "eslint": "^6.4.0",
     "eslint-config-standard": "^14.1.0",
+    "eslint-plugin-compat": "^3.3.0",
     "eslint-plugin-import": "^2.18.2",
     "eslint-plugin-node": "^10.0.0",
     "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-standard": "^4.0.1",
+    "gulp": "^4.0.2",
+    "gulp-buble": "^0.9.0",
+    "gulp-cli": "^2.2.0",
+    "gulp-eslint": "^6.0.0",
+    "gulp-if": "^3.0.0",
+    "gulp-nodemon": "^2.4.2",
+    "gulp-postcss": "^8.0.0",
+    "gulp-sourcemaps": "^2.6.5",
+    "gulp-stylelint": "^9.0.0",
+    "gulp-terser": "^1.2.0",
+    "postcss-preset-env": "^6.7.0",
     "stylelint": "^10.1.0",
     "stylelint-config-standard": "^18.3.0"
   }

+ 0 - 31
public/css/auth.css

@@ -1,31 +0,0 @@
-input {
-  background: rgba(0, 0, 0, 0)
-}
-
-input,
-a {
-  border-left: 0;
-  border-top: 0;
-  border-right: 0;
-  border-radius: 0;
-  -webkit-box-shadow: 0 0 0;
-  box-shadow: 0 0 0
-}
-
-.select-wrapper {
-  text-align: center;
-  margin-bottom: 10px
-}
-
-#login .input {
-  border-top: 0;
-  border-right: 0;
-  border-left: 0;
-  border-radius: 0;
-  padding-right: calc(0.75em + 1px);
-  padding-left: calc(0.75em + 1px)
-}
-
-#login .control .button {
-  border-radius: 0
-}

+ 0 - 9
public/libs/fontello/LICENSE

@@ -1,15 +1,6 @@
 Font license info
 
 
-## Typicons
-
-   (c) Stephen Hutchings 2012
-
-   Author:    Stephen Hutchings
-   License:   SIL (http://scripts.sil.org/OFL)
-   Homepage:  http://typicons.com/
-
-
 ## Elusive
 
    Copyright (C) 2013 by Aristeides Stathopoulos

File diff suppressed because it is too large
+ 240 - 0
public/libs/fontello/config.json


+ 27 - 26
public/libs/fontello/fontello.css

@@ -1,11 +1,11 @@
 @font-face {
   font-family: 'fontello';
-  src: url('fontello.eot?tWLiAlAX5i');
-  src: url('fontello.eot?tWLiAlAX5i#iefix') format('embedded-opentype'),
-       url('fontello.woff2?tWLiAlAX5i') format('woff2'),
-       url('fontello.woff?tWLiAlAX5i') format('woff'),
-       url('fontello.ttf?tWLiAlAX5i') format('truetype'),
-       url('fontello.svg?tWLiAlAX5i#fontello') format('svg');
+  src: url('fontello.eot?fFS2CGH95j');
+  src: url('fontello.eot?fFS2CGH95j#iefix') format('embedded-opentype'),
+       url('fontello.woff2?fFS2CGH95j') format('woff2'),
+       url('fontello.woff?fFS2CGH95j') format('woff'),
+       url('fontello.ttf?fFS2CGH95j') format('truetype'),
+       url('fontello.svg?fFS2CGH95j#fontello') format('svg');
   font-weight: normal;
   font-style: normal;
 }
@@ -15,7 +15,7 @@
 @media screen and (-webkit-min-device-pixel-ratio:0) {
   @font-face {
     font-family: 'fontello';
-    src: url('fontello.svg?tWLiAlAX5i#fontello') format('svg');
+    src: url('fontello.svg?fFS2CGH95j#fontello') format('svg');
   }
 }
 */
@@ -59,35 +59,36 @@
   font-size: 2rem;
 }
 
-.icon-pencil-1:before { content: '\e800'; } /* '' */
+.icon-archive:before { content: '\e800'; } /* '' */
 .icon-sharex:before { content: '\e801'; } /* '' */
-.icon-upload-cloud:before { content: '\e802'; } /* '' */
-.icon-picture-1:before { content: '\e803'; } /* '' */
-.icon-th-list:before { content: '\e804'; } /* '' */
-.icon-trash:before { content: '\e805'; } /* '' */
-.icon-th-large:before { content: '\e806'; } /* '' */
-.icon-arrows-cw:before { content: '\e807'; } /* '' */
-.icon-plus:before { content: '\e808'; } /* '' */
-.icon-cancel:before { content: '\e809'; } /* '' */
-.icon-archive:before { content: '\e80a'; } /* '' */
-.icon-clipboard-1:before { content: '\e80b'; } /* '' */
-.icon-login:before { content: '\e80c'; } /* '' */
-.icon-home:before { content: '\e80d'; } /* '' */
-.icon-download:before { content: '\e80e'; } /* '' */
-.icon-help-circled:before { content: '\e80f'; } /* '' */
+.icon-picture:before { content: '\e802'; } /* '' */
+.icon-th-list:before { content: '\e803'; } /* '' */
+.icon-trash:before { content: '\e804'; } /* '' */
+.icon-cancel:before { content: '\e805'; } /* '' */
+.icon-arrows-cw:before { content: '\e806'; } /* '' */
+.icon-plus:before { content: '\e807'; } /* '' */
+.icon-clipboard:before { content: '\e808'; } /* '' */
+.icon-login:before { content: '\e809'; } /* '' */
+.icon-home:before { content: '\e80a'; } /* '' */
+.icon-gauge:before { content: '\e80b'; } /* '' */
+.icon-help-circled:before { content: '\e80d'; } /* '' */
+.icon-github-circled:before { content: '\e80e'; } /* '' */
+.icon-pencil:before { content: '\e80f'; } /* '' */
 .icon-terminal:before { content: '\e810'; } /* '' */
 .icon-hammer:before { content: '\e811'; } /* '' */
 .icon-block:before { content: '\e812'; } /* '' */
 .icon-link:before { content: '\e813'; } /* '' */
 .icon-cog-alt:before { content: '\e814'; } /* '' */
 .icon-floppy:before { content: '\e815'; } /* '' */
+.icon-user-plus:before { content: '\e816'; } /* '' */
 .icon-privatebin:before { content: '\e817'; } /* '' */
-.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-upload-cloud:before { content: '\e819'; } /* '' */
+.icon-th-large:before { content: '\e81a'; } /* '' */
+.icon-download:before { content: '\e81b'; } /* '' */
+.icon-gatsby:before { content: '\e81c'; } /* '' */
 .icon-filter:before { content: '\f0b0'; } /* '' */
 .icon-docs:before { content: '\f0c5'; } /* '' */
-.icon-gauge:before { content: '\f0e4'; } /* '' */
 .icon-doc-inv:before { content: '\f15b'; } /* '' */
-.icon-paper-plane-empty:before { content: '\f1d9'; } /* '' */
-.icon-user-plus:before { content: '\f234'; } /* '' */
+.icon-paper-plane:before { content: '\f1d8'; } /* '' */
 .icon-chrome:before { content: '\f268'; } /* '' */
 .icon-firefox:before { content: '\f269'; } /* '' */

BIN
public/libs/fontello/fontello.eot


File diff suppressed because it is too large
+ 24 - 22
public/libs/fontello/fontello.svg


BIN
public/libs/fontello/fontello.ttf


BIN
public/libs/fontello/fontello.woff


BIN
public/libs/fontello/fontello.woff2


+ 3 - 3
routes/album.js

@@ -1,9 +1,9 @@
-const config = require('./../config')
-const db = require('knex')(config.database)
+const routes = require('express').Router()
 const path = require('path')
 const paths = require('./../controllers/pathsController')
-const routes = require('express').Router()
 const utils = require('./../controllers/utilsController')
+const config = require('./../config')
+const db = require('knex')(config.database)
 
 const homeDomain = config.homeDomain || config.domain
 

+ 3 - 3
routes/api.js

@@ -1,10 +1,10 @@
-const config = require('./../config')
 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')
+const tokenController = require('./../controllers/tokenController')
+const uploadController = require('./../controllers/uploadController')
 const utilsController = require('./../controllers/utilsController')
+const config = require('./../config')
 
 routes.get('/check', (req, res, next) => {
   return res.json({

+ 1 - 1
routes/nojs.js

@@ -1,7 +1,7 @@
-const config = require('./../config')
 const routes = require('express').Router()
 const uploadController = require('./../controllers/uploadController')
 const utils = require('./../controllers/utilsController')
+const config = require('./../config')
 
 const renderOptions = {
   uploadDisabled: false,

+ 0 - 13
scripts/_utils.js

@@ -1,13 +0,0 @@
-module.exports = {
-  stripIndents (string) {
-    if (!string) return
-    const result = string.replace(/^[^\S\n]+/gm, '')
-    const match = result.match(/^[^\S\n]*(?=\S)/gm)
-    const indent = match && Math.min(...match.map(el => el.length))
-    if (indent) {
-      const regexp = new RegExp(`^.{${indent}}`, 'gm')
-      return result.replace(regexp, '')
-    }
-    return result
-  }
-}

+ 1 - 2
scripts/cf-purge.js

@@ -1,4 +1,3 @@
-const { stripIndents } = require('./_utils')
 const utils = require('./../controllers/utilsController')
 
 ;(async () => {
@@ -6,7 +5,7 @@ const utils = require('./../controllers/utilsController')
   const args = process.argv.slice(2)
 
   if (!args.length || args.includes('--help') || args.includes('-h'))
-    return console.log(stripIndents(`
+    return console.log(utils.stripIndents(`
       Purge Cloudflare's cache.
 
       Usage:

+ 4 - 4
scripts/clean-up.js

@@ -1,8 +1,8 @@
-const { stripIndents } = require('./_utils')
+const path = require('path')
+const paths = require('../controllers/pathsController')
+const utils = require('../controllers/utilsController')
 const config = require('./../config')
 const db = require('knex')(config.database)
-const path = require('path')
-const paths = require('./../controllers/pathsController')
 
 const self = {
   mode: null
@@ -26,7 +26,7 @@ self.getFiles = async directory => {
   self.mode = parseInt(args[0]) || 0
 
   if (args.includes('--help') || args.includes('-h'))
-    return console.log(stripIndents(`
+    return console.log(utils.stripIndents(`
       Clean up files that are not in the database.
 
       Usage:

+ 2 - 3
scripts/delete-expired.js

@@ -1,5 +1,4 @@
-const { stripIndents } = require('./_utils')
-const utils = require('./../controllers/utilsController')
+const utils = require('../controllers/utilsController')
 
 const self = {
   mode: null
@@ -12,7 +11,7 @@ const self = {
   self.mode = parseInt(args[0]) || 0
 
   if (args.includes('--help') || args.includes('-h'))
-    return console.log(stripIndents(`
+    return console.log(utils.stripIndents(`
       Bulk delete expired files.
 
       Usage:

+ 3 - 4
scripts/thumbs.js

@@ -1,7 +1,6 @@
-const { stripIndents } = require('./_utils')
 const path = require('path')
-const paths = require('./../controllers/pathsController')
-const utils = require('./../controllers/utilsController')
+const paths = require('../controllers/pathsController')
+const utils = require('../controllers/utilsController')
 
 const self = {
   mode: null,
@@ -40,7 +39,7 @@ self.getFiles = async directory => {
     ![0, 1].includes(self.verbose) ||
     args.includes('--help') ||
     args.includes('-h'))
-    return console.log(stripIndents(`
+    return console.log(utils.stripIndents(`
       Generate thumbnails.
 
       Usage  :

public/css/album.css → src/css/album.css


+ 13 - 14
public/css/dashboard.css

@@ -1,10 +1,8 @@
 body {
-  -webkit-animation: none;
   animation: none
 }
 
 #dashboard {
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -13,12 +11,7 @@ body {
 }
 
 .menu-list a {
-  color: #3794d2;
-  -webkit-touch-callout: none;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none
+  color: #3794d2
 }
 
 .menu-list a:hover {
@@ -48,15 +41,17 @@ ul#albumsContainer {
 ul#albumsContainer li {
   border-left: 1px solid #898b8d;
   padding-left: 0.75em;
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
 #page.fade-in {
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
+.pagination {
+  margin-bottom: 1.25rem
+}
+
 .pagination a:not([disabled]) {
   color: #eff0f1;
   border-color: #4d4d4d;
@@ -118,10 +113,6 @@ li[data-action="page-ellipsis"] {
   min-width: 0
 }
 
-.table-container {
-  overflow-x: auto
-}
-
 .table {
   color: #bdc3c7;
   background-color: #31363b;
@@ -163,3 +154,11 @@ li[data-action="page-ellipsis"] {
 .is-linethrough {
   text-decoration: line-through
 }
+
+#menu.is-loading li a {
+  cursor: progress
+}
+
+#statistics tr *:nth-child(2) {
+  min-width: 50%
+}

+ 2 - 31
public/css/home.css

@@ -4,19 +4,12 @@
   border-radius: 100%;
   display: inline-block;
   margin-bottom: 40px;
-  position: relative;
   vertical-align: top;
-  -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);
-  -webkit-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);
   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)
 }
 
@@ -38,15 +31,11 @@
 }
 
 .dz-preview .dz-details {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex
 }
 
 .dz-preview .dz-details .dz-size,
 .dz-preview .dz-details .dz-filename {
-  -webkit-box-flex: 1;
-  -ms-flex: 1;
   flex: 1
 }
 
@@ -59,9 +48,7 @@
 @-webkit-keyframes floatUp {
   0% {
     opacity: 0;
-    -webkit-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);
     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)
   }
 
@@ -70,16 +57,12 @@
   }
 
   67% {
-    -webkit-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);
     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% {
-    -webkit-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);
     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)
   }
 }
@@ -87,9 +70,7 @@
 @keyframes floatUp {
   0% {
     opacity: 0;
-    -webkit-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);
     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)
   }
 
@@ -98,22 +79,17 @@
   }
 
   67% {
-    -webkit-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);
     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% {
-    -webkit-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);
     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)
   }
 }
 
 .uploads > div {
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s;
   margin: 1rem
 }
@@ -126,11 +102,11 @@
   margin-bottom: 0
 }
 
-.uploads .icon:not(.icon-block) {
+.uploads .field > .icon:not(.icon-block) {
   color: #3794d2
 }
 
-.uploads .icon.icon-block {
+.uploads .field > .icon.icon-block {
   color: #da4453
 }
 
@@ -158,7 +134,6 @@
 }
 
 #albumDiv {
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -170,7 +145,6 @@
   margin-top: -0.25rem;
   margin-left: -0.25rem;
   margin-right: -0.25rem;
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -190,7 +164,6 @@
 
 #tabs {
   margin-bottom: 1rem;
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -216,7 +189,6 @@
 
 .tab-content {
   margin-bottom: -0.75rem;
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -247,7 +219,6 @@
   border-bottom-right-radius: 0;
   right: 1%;
   opacity: 0.25;
-  -webkit-transition: opacity 0.25s;
   transition: opacity 0.25s
 }
 

+ 20 - 31
public/css/style.css

@@ -5,7 +5,6 @@ html {
 
 body {
   color: #eff0f1;
-  -webkit-animation: fadeInOpacity 0.5s;
   animation: fadeInOpacity 0.5s
 }
 
@@ -103,30 +102,11 @@ code,
   color: #7f8c8d
 }
 
-.button.is-breeze {
-  background-color: #3794d2;
-  border-color: transparent;
-  color: #fff
-}
-
-.button.is-breeze.is-hovered,
-.button.is-breeze:hover {
-  background-color: #60a8dc;
-  border-color: transparent;
-  color: #fff
-}
-
-.button.is-breeze.is-active,
-.button.is-breeze:active {
-  background-color: #60a8dc;
-  border-color: transparent;
-  color: #fff
-}
-
-.button.is-breeze.is-focus,
-.button.is-breeze:focus {
-  border-color: transparent;
-  color: #fff
+.button.is-info.is-hovered [class^="icon-"]::before,
+.button.is-info.is-hovered [class*=" icon-"]::before,
+.button.is-info:hover [class^="icon-"]::before,
+.button.is-info:hover [class*=" icon-"]::before {
+  fill: #fff
 }
 
 .checkbox:hover,
@@ -134,11 +114,6 @@ code,
   color: #7f8c8d
 }
 
-.progress.is-breeze:indeterminate {
-  background-image: -webkit-gradient(linear, left top, right top, color-stop(30%, #60a8dc), color-stop(30%, #eff0f1));
-  background-image: linear-gradient(to right, #60a8dc 30%, #eff0f1 30%)
-}
-
 .message {
   background-color: #31363b
 }
@@ -146,6 +121,20 @@ code,
 .message-body {
   color: #eff0f1;
   border: 0;
-  -webkit-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);
   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)
 }
+
+.menu-list a.is-loading::after {
+  animation: spinAround 0.5s infinite linear;
+  border: 2px solid #dbdbdb;
+  border-radius: 290486px;
+  border-right-color: transparent;
+  border-top-color: transparent;
+  content: "";
+  display: block;
+  height: 1em;
+  width: 1em;
+  right: calc(0% + (1em / 2));
+  top: calc(50% - (1em / 2));
+  position: absolute !important
+}

+ 0 - 9
public/css/sweetalert.css

@@ -41,7 +41,6 @@
 }
 
 .swal-button:focus {
-  -webkit-box-shadow: 0 0 0 1px #31363b, 0 0 0 3px rgba(55, 148, 210, 0.29);
   box-shadow: 0 0 0 1px #31363b, 0 0 0 3px rgba(55, 148, 210, 0.29)
 }
 
@@ -72,14 +71,12 @@
 
 .swal-icon--warning {
   border-color: #f67400;
-  -webkit-animation: pulseWarning 0.5s infinite alternate;
   animation: pulseWarning 0.5s infinite alternate
 }
 
 .swal-icon--warning__body,
 .swal-icon--warning__dot {
   background-color: #f67400;
-  -webkit-animation: pulseWarningBody 0.5s infinite alternate;
   animation: pulseWarningBody 0.5s infinite alternate
 }
 
@@ -143,13 +140,7 @@
 .swal-display-thumb-container {
   min-width: 200px;
   min-height: 200px;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
   align-items: center;
-  -webkit-box-pack: center;
-  -ms-flex-pack: center;
   justify-content: center
 }

+ 0 - 14
public/css/thumbs.css

@@ -1,17 +1,11 @@
 .image-container {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   width: 200px;
   height: 200px;
   margin: 9px;
   background-color: #31363b;
   overflow: hidden;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
   align-items: center;
-  position: relative;
-  -webkit-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);
   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)
 }
 
@@ -21,16 +15,10 @@
 }
 
 .image-container .image {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   height: 100%;
   width: 100%;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
   align-items: center;
-  -webkit-box-pack: center;
-  -ms-flex-pack: center;
   justify-content: center
 }
 
@@ -42,8 +30,6 @@
 }
 
 .image-container .controls {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   position: absolute;
   top: 0.75rem;

+ 4 - 2
public/js/.eslintrc.json

@@ -1,13 +1,15 @@
 {
   "root": true,
   "parserOptions": {
-    "ecmaVersion": 6
+    "ecmaVersion": 6,
+    "sourceType": "script"
   },
   "env": {
     "browser": true
   },
   "extends": [
-    "standard"
+    "standard",
+    "plugin:compat/recommended"
   ],
   "rules": {
     "curly": [

+ 1 - 1
public/js/album.js

@@ -7,7 +7,7 @@ const page = {
   lazyLoad: null
 }
 
-window.onload = function () {
+window.onload = () => {
   const elements = document.querySelectorAll('.file-size')
   for (let i = 0; i < elements.length; i++)
     elements[i].innerHTML = page.getPrettyBytes(parseInt(elements[i].innerHTML.replace(/\s*B$/i, '')))

+ 17 - 13
public/js/auth.js

@@ -13,7 +13,7 @@ const page = {
   pass: null
 }
 
-page.do = function (dest) {
+page.do = (dest, trigger) => {
   const user = page.user.value.trim()
   if (!user)
     return swal('An error occurred!', 'You need to specify a username.', 'error')
@@ -22,32 +22,36 @@ page.do = function (dest) {
   if (!pass)
     return swal('An error occurred!', 'You need to specify a password.', 'error')
 
+  trigger.classList.add('is-loading')
   axios.post(`api/${dest}`, {
     username: user,
     password: pass
-  }).then(function (response) {
-    if (response.data.success === false)
+  }).then(response => {
+    if (response.data.success === false) {
+      trigger.classList.remove('is-loading')
       return swal(`Unable to ${dest}!`, response.data.description, 'error')
+    }
 
     localStorage.token = response.data.token
     window.location = 'dashboard'
-  }).catch(function (error) {
+  }).catch(error => {
     console.error(error)
+    trigger.classList.remove('is-loading')
     return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
   })
 }
 
-page.verify = function () {
+page.verify = () => {
   if (!page.token) return
 
   axios.post('api/tokens/verify', {
     token: page.token
-  }).then(function (response) {
+  }).then(response => {
     if (response.data.success === false)
       return swal('An error occurred!', response.data.description, 'error')
 
     window.location = 'dashboard'
-  }).catch(function (error) {
+  }).catch(error => {
     console.error(error)
     const description = error.response.data && error.response.data.description
       ? error.response.data.description
@@ -56,22 +60,22 @@ page.verify = function () {
   })
 }
 
-window.onload = function () {
+window.onload = () => {
   page.verify()
 
   page.user = document.querySelector('#user')
   page.pass = document.querySelector('#pass')
 
   // Prevent default form's submit action
-  document.querySelector('#authForm').addEventListener('submit', function (event) {
+  document.querySelector('#authForm').addEventListener('submit', event => {
     event.preventDefault()
   })
 
-  document.querySelector('#loginBtn').addEventListener('click', function () {
-    page.do('login')
+  document.querySelector('#loginBtn').addEventListener('click', event => {
+    page.do('login', event.currentTarget)
   })
 
-  document.querySelector('#registerBtn').addEventListener('click', function () {
-    page.do('register')
+  document.querySelector('#registerBtn').addEventListener('click', event => {
+    page.do('register', event.currentTarget)
   })
 }

File diff suppressed because it is too large
+ 453 - 357
public/js/dashboard.js


+ 180 - 149
public/js/home.js

@@ -31,7 +31,7 @@ const page = {
   urlMaxSize: null,
   urlMaxSizeBytes: null,
 
-  tabs: null,
+  tabs: [],
   activeTab: null,
   albumSelect: null,
   previewTemplate: null,
@@ -43,56 +43,46 @@ const page = {
   imageExtensions: ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png', '.svg']
 }
 
-page.checkIfPublic = function () {
-  axios.get('api/check').then(function (response) {
-    page.private = response.data.private
-    page.enableUserAccounts = response.data.enableUserAccounts
-    page.maxSize = parseInt(response.data.maxSize)
-    page.maxSizeBytes = page.maxSize * 1e6
-    page.chunkSize = parseInt(response.data.chunkSize)
-    page.temporaryUploadAges = response.data.temporaryUploadAges
-    page.fileIdentifierLength = response.data.fileIdentifierLength
-    page.preparePage()
-  }).catch(function (error) {
-    console.error(error)
-    document.querySelector('#albumDiv').classList.add('is-hidden')
-    document.querySelector('#tabs').classList.add('is-hidden')
-    const button = document.querySelector('#loginToUpload')
-    button.innerText = 'Error occurred. Reload the page?'
-    button.classList.remove('is-loading')
-    button.classList.remove('is-hidden')
-    return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
-  })
+page.checkIfPublic = onFailure => {
+  return axios.get('api/check')
+    .then(response => {
+      page.private = response.data.private
+      page.enableUserAccounts = response.data.enableUserAccounts
+      page.maxSize = parseInt(response.data.maxSize)
+      page.maxSizeBytes = page.maxSize * 1e6
+      page.chunkSize = parseInt(response.data.chunkSize)
+      page.temporaryUploadAges = response.data.temporaryUploadAges
+      page.fileIdentifierLength = response.data.fileIdentifierLength
+      return page.preparePage(onFailure)
+    })
+    .catch(onFailure)
 }
 
-page.preparePage = function () {
+page.preparePage = onFailure => {
   if (page.private)
     if (page.token) {
-      return page.verifyToken(page.token, true)
+      return page.verifyToken(page.token, onFailure, true)
     } else {
       const button = document.querySelector('#loginToUpload')
       button.href = 'auth'
       button.classList.remove('is-loading')
-
       if (page.enableUserAccounts)
         button.innerText = 'Anonymous upload is disabled. Log in to upload.'
       else
         button.innerText = 'Running in private mode. Log in to upload.'
     }
   else
-    return page.prepareUpload()
+    return page.prepareUpload(onFailure)
 }
 
-page.verifyToken = function (token, reloadOnError) {
-  if (reloadOnError === undefined) reloadOnError = false
-
-  axios.post('api/tokens/verify', { token }).then(function (response) {
+page.verifyToken = (token, onFailure, reloadOnError) => {
+  return axios.post('api/tokens/verify', { token }).then(response => {
     if (response.data.success === false)
       return swal({
         title: 'An error occurred!',
         text: response.data.description,
         icon: 'error'
-      }).then(function () {
+      }).then(() => {
         if (!reloadOnError) return
         localStorage.removeItem('token')
         location.reload()
@@ -100,120 +90,113 @@ page.verifyToken = function (token, reloadOnError) {
 
     localStorage.token = token
     page.token = token
-    return page.prepareUpload()
-  }).catch(function (error) {
-    console.error(error)
-    return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
-  })
+    return page.prepareUpload(onFailure)
+  }).catch(onFailure)
 }
 
-page.prepareUpload = function () {
+page.prepareUpload = onFailure => {
   // I think this fits best here because we need to check for a valid token before we can get the albums
   if (page.token) {
+    // Display the album selection
+    document.querySelector('#albumDiv').classList.remove('is-hidden')
+
     page.albumSelect = document.querySelector('#albumSelect')
-    page.albumSelect.addEventListener('change', function () {
+    page.albumSelect.addEventListener('change', () => {
       page.album = parseInt(page.albumSelect.value)
       // Re-generate ShareX config file
       if (typeof page.prepareShareX === 'function')
         page.prepareShareX()
     })
 
-    page.prepareAlbums()
-
-    // Display the album selection
-    document.querySelector('#albumDiv').classList.remove('is-hidden')
+    // Fetch albums
+    page.fetchAlbums(onFailure)
   }
 
+  // Prepare & generate config tab
   page.prepareUploadConfig()
 
-  document.querySelector('#maxSize').innerHTML = `Maximum upload size per file is ${page.getPrettyBytes(page.maxSizeBytes)}`
+  // Update elements wherever applicable
+  document.querySelector('#maxSize > span').innerHTML = page.getPrettyBytes(page.maxSizeBytes)
   document.querySelector('#loginToUpload').classList.add('is-hidden')
 
   if (!page.token && page.enableUserAccounts)
     document.querySelector('#loginLinkText').innerHTML = 'Create an account and keep track of your uploads'
 
-  const previewNode = document.querySelector('#tpl')
-  page.previewTemplate = previewNode.innerHTML
-  previewNode.parentNode.removeChild(previewNode)
-
+  // Prepare & generate files upload tab
   page.prepareDropzone()
 
   // Generate ShareX config file
   if (typeof page.prepareShareX === 'function')
     page.prepareShareX()
 
+  // Prepare urls upload tab
   const urlMaxSize = document.querySelector('#urlMaxSize')
   if (urlMaxSize) {
     page.urlMaxSize = parseInt(urlMaxSize.innerHTML)
     page.urlMaxSizeBytes = page.urlMaxSize * 1e6
     urlMaxSize.innerHTML = page.getPrettyBytes(page.urlMaxSizeBytes)
-    document.querySelector('#uploadUrls').addEventListener('click', function () {
-      page.uploadUrls(this)
+    document.querySelector('#uploadUrls').addEventListener('click', event => {
+      page.uploadUrls(event.currentTarget)
     })
   }
 
-  const tabs = document.querySelector('#tabs')
-  page.tabs = tabs.querySelectorAll('li')
-  for (let i = 0; i < page.tabs.length; i++)
-    page.tabs[i].addEventListener('click', function () {
-      page.setActiveTab(this.dataset.id)
+  // Get all tabs
+  const tabsContainer = document.querySelector('#tabs')
+  const tabs = tabsContainer.querySelectorAll('li')
+  for (let i = 0; i < tabs.length; i++) {
+    const id = tabs[i].dataset.id
+    const tabContent = document.querySelector(`#${id}`)
+    if (!tabContent) continue
+
+    tabs[i].addEventListener('click', () => {
+      page.setActiveTab(i)
     })
-  page.setActiveTab('tab-files')
-  tabs.classList.remove('is-hidden')
-}
+    page.tabs.push({ tab: tabs[i], content: tabContent })
+  }
 
-page.prepareAlbums = function () {
-  const option = document.createElement('option')
-  option.value = ''
-  option.innerHTML = 'Upload to album'
-  option.selected = true
-  page.albumSelect.appendChild(option)
+  // Set first valid tab as the default active tab
+  if (page.tabs.length) {
+    page.setActiveTab(0)
+    tabsContainer.classList.remove('is-hidden')
+  }
+}
 
-  axios.get('api/albums', {
-    headers: {
-      token: page.token
+page.setActiveTab = index => {
+  for (let i = 0; i < page.tabs.length; i++)
+    if (i === index) {
+      page.tabs[i].tab.classList.add('is-active')
+      page.tabs[i].content.classList.remove('is-hidden')
+      page.activeTab = index
+    } else {
+      page.tabs[i].tab.classList.remove('is-active')
+      page.tabs[i].content.classList.add('is-hidden')
     }
-  }).then(function (response) {
+}
+
+page.fetchAlbums = onFailure => {
+  return axios.get('api/albums', { headers: { token: page.token } }).then(response => {
     if (response.data.success === false)
       return swal('An error occurred!', response.data.description, 'error')
 
-    // If the user doesn't have any albums we don't really need to display
-    // an album selection
-    if (!response.data.albums.length) return
-
-    // Loop through the albums and create an option for each album
-    for (let i = 0; i < response.data.albums.length; i++) {
-      const album = response.data.albums[i]
-      const option = document.createElement('option')
-      option.value = album.id
-      option.innerHTML = album.name
-      page.albumSelect.appendChild(option)
-    }
-  }).catch(function (error) {
-    console.error(error)
-    const description = error.response.data && error.response.data.description
-      ? error.response.data.description
-      : 'There was an error with the request, please check the console for more information.'
-    return swal(`${error.response.status} ${error.response.statusText}`, description, 'error')
-  })
+    // Create an option for each album
+    if (Array.isArray(response.data.albums) && response.data.albums.length)
+      for (let i = 0; i < response.data.albums.length; i++) {
+        const album = response.data.albums[i]
+        const option = document.createElement('option')
+        option.value = album.id
+        option.innerHTML = album.name
+        page.albumSelect.appendChild(option)
+      }
+  }).catch(onFailure)
 }
 
-page.setActiveTab = function (tabId) {
-  if (tabId === page.activeTab) return
-  for (let i = 0; i < page.tabs.length; i++) {
-    const id = page.tabs[i].dataset.id
-    if (id === tabId) {
-      page.tabs[i].classList.add('is-active')
-      document.querySelector(`#${id}`).classList.remove('is-hidden')
-    } else {
-      page.tabs[i].classList.remove('is-active')
-      document.querySelector(`#${id}`).classList.add('is-hidden')
-    }
-  }
-  page.activeTab = tabId
-}
+page.prepareDropzone = () => {
+  // Parse template element
+  const previewNode = document.querySelector('#tpl')
+  page.previewTemplate = previewNode.innerHTML
+  previewNode.parentNode.removeChild(previewNode)
 
-page.prepareDropzone = function () {
+  // Generate files upload tab
   const tabDiv = document.querySelector('#tab-files')
   const div = document.createElement('div')
   div.className = 'control is-expanded'
@@ -258,17 +241,15 @@ page.prepareDropzone = function () {
           filelength: page.fileLength,
           age: page.uploadAge
         }]
-      }, {
-        headers: { token: page.token }
-      }).catch(function (error) {
-        if (error.response.data) return error.response
-        return {
+      }, { headers: { token: page.token } }).catch(error => {
+        // Format error for display purpose
+        return error.response.data ? error.response : {
           data: {
             success: false,
             description: error.toString()
           }
         }
-      }).then(function (response) {
+      }).then(response => {
         file.previewElement.querySelector('.progress').classList.add('is-hidden')
 
         if (response.data.success === false)
@@ -282,15 +263,16 @@ page.prepareDropzone = function () {
     }
   })
 
-  page.dropzone.on('addedfile', function (file) {
-    // Set active tab to file uploads
-    page.setActiveTab('tab-files')
+  page.dropzone.on('addedfile', file => {
+    // Set active tab to file uploads, if necessary
+    if (page.activeTab !== 0)
+      page.setActiveTab(0)
     // Add file entry
     tabDiv.querySelector('.uploads').classList.remove('is-hidden')
     file.previewElement.querySelector('.name').innerHTML = file.name
   })
 
-  page.dropzone.on('sending', function (file, xhr) {
+  page.dropzone.on('sending', (file, xhr) => {
     if (file.upload.chunked) return
     // Add headers if not uploading chunks
     if (page.album !== null) xhr.setRequestHeader('albumid', page.album)
@@ -299,7 +281,7 @@ page.prepareDropzone = function () {
   })
 
   // Update the total progress bar
-  page.dropzone.on('uploadprogress', function (file, progress) {
+  page.dropzone.on('uploadprogress', (file, progress) => {
     // For some reason, chunked uploads fire 100% progress event
     // for each chunk's successful uploads
     if (file.upload.chunked && progress === 100) return
@@ -307,7 +289,7 @@ page.prepareDropzone = function () {
     file.previewElement.querySelector('.progress').innerHTML = `${progress}%`
   })
 
-  page.dropzone.on('success', function (file, response) {
+  page.dropzone.on('success', (file, response) => {
     if (!response) return
     file.previewElement.querySelector('.progress').classList.add('is-hidden')
 
@@ -318,7 +300,7 @@ page.prepareDropzone = function () {
       page.updateTemplate(file, response.files[0])
   })
 
-  page.dropzone.on('error', function (file, error) {
+  page.dropzone.on('error', (file, error) => {
     // Clean up file size errors
     if ((typeof error === 'string' && /^File is too big/.test(error)) ||
       (typeof error === 'object' && /File too large/.test(error.description)))
@@ -331,11 +313,11 @@ page.prepareDropzone = function () {
   })
 }
 
-page.uploadUrls = function (button) {
+page.uploadUrls = button => {
   const tabDiv = document.querySelector('#tab-urls')
-  if (!tabDiv) return
+  if (!tabDiv || button.classList.contains('is-loading'))
+    return
 
-  if (button.classList.contains('is-loading')) return
   button.classList.add('is-loading')
 
   function done (error) {
@@ -354,17 +336,16 @@ page.uploadUrls = function (button) {
     const previewsContainer = tabDiv.querySelector('.uploads')
     const urls = document.querySelector('#urls').value
       .split(/\r?\n/)
-      .filter(function (url) {
+      .filter(url => {
         return url.trim().length
       })
     document.querySelector('#urls').value = urls.join('\n')
 
     if (!urls.length)
-      // eslint-disable-next-line prefer-promise-reject-errors
       return done('You have not entered any URLs.')
 
     tabDiv.querySelector('.uploads').classList.remove('is-hidden')
-    const files = urls.map(function (url) {
+    const files = urls.map(url => {
       const previewTemplate = document.createElement('template')
       previewTemplate.innerHTML = page.previewTemplate.trim()
       const previewElement = previewTemplate.content.firstChild
@@ -391,9 +372,9 @@ page.uploadUrls = function (button) {
       // Animate progress bar
       files[i].previewElement.querySelector('.progress').removeAttribute('value')
 
-      axios.post('api/upload', { urls: [files[i].url] }, { headers }).then(function (response) {
+      return axios.post('api/upload', { urls: [files[i].url] }, { headers }).then(response => {
         return posted(response.data)
-      }).catch(function (error) {
+      }).catch(error => {
         return posted({
           success: false,
           description: error.response ? error.response.data.description : error.toString()
@@ -405,14 +386,14 @@ page.uploadUrls = function (button) {
   return run()
 }
 
-page.updateTemplateIcon = function (templateElement, iconClass) {
+page.updateTemplateIcon = (templateElement, iconClass) => {
   const iconElement = templateElement.querySelector('.icon')
   if (!iconElement) return
   iconElement.classList.add(iconClass)
   iconElement.classList.remove('is-hidden')
 }
 
-page.updateTemplate = function (file, response) {
+page.updateTemplate = (file, response) => {
   if (!response.url) return
 
   const a = file.previewElement.querySelector('.link > a')
@@ -426,11 +407,11 @@ page.updateTemplate = function (file, response) {
     img.setAttribute('alt', response.name || '')
     img.dataset.src = response.url
     img.classList.remove('is-hidden')
-    img.onerror = function () {
+    img.onerror = event => {
       // Hide image elements that fail to load
-      // Consequently include WEBP in browsers that do not have WEBP support (Firefox/IE)
-      this.classList.add('is-hidden')
-      file.previewElement.querySelector('.icon').classList.remove('is-hidden')
+      // Consequently include WEBP in browsers that do not have WEBP support (e.i. IE)
+      event.currentTarget.classList.add('is-hidden')
+      page.updateTemplateIcon(file.previewElement, 'icon-picture')
     }
     page.lazyLoad.update(file.previewElement.querySelectorAll('img'))
   } else {
@@ -444,7 +425,7 @@ page.updateTemplate = function (file, response) {
   }
 }
 
-page.createAlbum = function () {
+page.createAlbum = () => {
   const div = document.createElement('div')
   div.innerHTML = `
     <div class="field">
@@ -485,7 +466,7 @@ page.createAlbum = function () {
         closeModal: false
       }
     }
-  }).then(function (value) {
+  }).then(value => {
     if (!value) return
 
     const name = document.querySelector('#swalName').value.trim()
@@ -498,7 +479,7 @@ page.createAlbum = function () {
       headers: {
         token: page.token
       }
-    }).then(function (response) {
+    }).then(response => {
       if (response.data.success === false)
         return swal('An error occurred!', response.data.description, 'error')
 
@@ -509,14 +490,14 @@ page.createAlbum = function () {
       option.selected = true
 
       swal('Woohoo!', 'Album was created successfully.', 'success')
-    }).catch(function (error) {
+    }).catch(error => {
       console.error(error)
       return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
     })
   })
 }
 
-page.prepareUploadConfig = function () {
+page.prepareUploadConfig = () => {
   const fallback = {
     chunkSize: page.chunkSize,
     parallelUploads: 2
@@ -529,13 +510,13 @@ page.prepareUploadConfig = function () {
 
   const numConfig = {
     chunkSize: { min: 1, max: 95 },
-    parallelUploads: { min: 1, max: Number.MAX_SAFE_INTEGER }
+    parallelUploads: { min: 1, max: 8 }
   }
 
   document.querySelector('#chunkSizeDiv .help').innerHTML =
-    `Default is ${fallback.chunkSize} MB. Max is ${numConfig.chunkSize.max}.`
+    `Default is ${fallback.chunkSize} MB. Max is ${numConfig.chunkSize.max} MB.`
   document.querySelector('#parallelUploadsDiv .help').innerHTML =
-    `Default is ${fallback.parallelUploads}.`
+    `Default is ${fallback.parallelUploads}. Max is ${numConfig.parallelUploads.max}.`
 
   const fileLengthDiv = document.querySelector('#fileLengthDiv')
   if (page.fileIdentifierLength && fileLengthDiv) {
@@ -577,7 +558,7 @@ page.prepareUploadConfig = function () {
     fileLengthDiv.querySelector('.help').innerHTML = helpText
   }
 
-  Object.keys(numConfig).forEach(function (key) {
+  Object.keys(numConfig).forEach(key => {
     document.querySelector(`#${key}`).setAttribute('min', numConfig[key].min)
     document.querySelector(`#${key}`).setAttribute('max', numConfig[key].max)
   })
@@ -603,14 +584,14 @@ page.prepareUploadConfig = function () {
 
   const tabContent = document.querySelector('#tab-config')
   const form = tabContent.querySelector('form')
-  form.addEventListener('submit', function (event) {
+  form.addEventListener('submit', event => {
     event.preventDefault()
   })
 
   const siBytes = localStorage[lsKeys.siBytes] !== '0'
   if (!siBytes) document.querySelector('#siBytes').value = '0'
 
-  document.querySelector('#saveConfig').addEventListener('click', function () {
+  document.querySelector('#saveConfig').addEventListener('click', () => {
     if (!form.checkValidity())
       return
 
@@ -637,13 +618,13 @@ page.prepareUploadConfig = function () {
       title: 'Woohoo!',
       text: 'Configuration saved into this browser.',
       icon: 'success'
-    }).then(function () {
+    }).then(() => {
       location.reload()
     })
   })
 }
 
-page.getPrettyUploadAge = function (hours) {
+page.getPrettyUploadAge = hours => {
   if (hours === 0) {
     return 'Permanent'
   } else if (hours < 1) {
@@ -658,7 +639,7 @@ page.getPrettyUploadAge = function (hours) {
 }
 
 // Handle image paste event
-window.addEventListener('paste', function (event) {
+window.addEventListener('paste', event => {
   const items = (event.clipboardData || event.originalEvent.clipboardData).items
   const index = Object.keys(items)
   for (let i = 0; i < index.length; i++) {
@@ -673,25 +654,75 @@ window.addEventListener('paste', function (event) {
   }
 })
 
-window.onload = function () {
-  page.checkIfPublic()
+window.onload = () => {
+  // Global error callback
+  function onFailure (error) {
+    if (error === undefined) return
+    console.error(error)
+
+    // Hide these elements
+    document.querySelector('#albumDiv').classList.add('is-hidden')
+    document.querySelector('#tabs').classList.add('is-hidden')
+    document.querySelectorAll('.tab-content').forEach(element => {
+      return element.classList.add('is-hidden')
+    })
+
+    // Update upload button
+    const uploadButton = document.querySelector('#loginToUpload')
+    uploadButton.innerText = 'An error occurred. Try to reload?'
+    uploadButton.classList.remove('is-loading')
+    uploadButton.classList.remove('is-hidden')
+
+    uploadButton.addEventListener('click', () => {
+      location.reload()
+    })
+
+    // Show alert modal
+    if (error.response) {
+      console.error(error.response)
+      // Better error messages for Cloudflare errors
+      const cloudflareErrors = {
+        520: 'Unknown Error',
+        521: 'Web Server Is Down',
+        522: 'Connection Timed Out',
+        523: 'Origin Is Unreachable',
+        524: 'A Timeout Occurred',
+        525: 'SSL Handshake Failed',
+        526: 'Invalid SSL Certificate',
+        527: 'Railgun Error',
+        530: 'Origin DNS Error'
+      }
+      const statusText = cloudflareErrors[error.response.status] || error.response.statusText
+      const description = error.response.data && error.response.data.description
+        ? error.response.data.description
+        : 'Please check the console for more information.'
+      return swal(`${error.response.status} ${statusText}`, description, 'error')
+    } else {
+      const content = document.createElement('div')
+      content.innerHTML = `<code>${error.toString()}</code>`
+      return swal({
+        title: 'An error occurred!',
+        icon: 'error',
+        content
+      })
+    }
+  }
+
+  page.checkIfPublic(onFailure)
 
   page.clipboardJS = new ClipboardJS('.clipboard-js')
 
-  page.clipboardJS.on('success', function () {
+  page.clipboardJS.on('success', () => {
     return swal('Copied!', 'The link has been copied to clipboard.', 'success')
   })
 
-  page.clipboardJS.on('error', function (event) {
-    console.error(event)
-    return swal('An error occurred!', 'There was an error when trying to copy the link to clipboard, please check the console for more information.', 'error')
-  })
+  page.clipboardJS.on('error', onFailure)
 
   page.lazyLoad = new LazyLoad({
     elements_selector: '.field.uploads img'
   })
 
-  document.querySelector('#createAlbum').addEventListener('click', function () {
+  document.querySelector('#createAlbum').addEventListener('click', () => {
     page.createAlbum()
   })
 }

+ 6 - 6
public/js/s/render.js

@@ -63,7 +63,7 @@ for (let i = 1; i <= 50; i++)
 page.config = null
 page.render = null
 
-page.doRenderSwal = function () {
+page.doRenderSwal = () => {
   const div = document.createElement('div')
   div.innerHTML = `
     <div class="field">
@@ -82,7 +82,7 @@ page.doRenderSwal = function () {
     buttons: {
       confirm: true
     }
-  }).then(function (value) {
+  }).then(value => {
     if (!value) return
     const newValue = div.querySelector('#swalRender').checked ? undefined : '0'
     if (newValue !== localStorage[lsKeys.render]) {
@@ -98,23 +98,23 @@ page.doRenderSwal = function () {
   })
 }
 
-page.getRenderVersion = function () {
+page.getRenderVersion = () => {
   const renderScript = document.querySelector('#renderScript')
   if (renderScript && renderScript.dataset.version)
     return `?v=${renderScript.dataset.version}`
   return ''
 }
 
-page.doRender = function () {
+page.doRender = () => {
   page.config = page.renderConfig[page.renderType]
   if (!page.config || !page.config.array.length) return
 
   let element
   if (localStorage[lsKeys.render] === '0') {
     element = document.createElement('a')
-    element.className = 'button is-breeze is-hidden-mobile'
+    element.className = 'button is-info is-hidden-mobile'
     element.title = page.config.name
-    element.innerHTML = '<i class="icon-picture-1"></i>'
+    element.innerHTML = '<i class="icon-picture"></i>'
   } else {
     // Let us just allow people to get new render when toggling the option
     page.render = page.config.array[Math.floor(Math.random() * page.config.array.length)]

+ 5 - 5
public/js/s/utils.js

@@ -3,7 +3,7 @@
 // keys for localStorage
 lsKeys.siBytes = 'siBytes'
 
-page.prepareShareX = function () {
+page.prepareShareX = () => {
   const values = page.token ? {
     token: page.token || '',
     albumid: page.album || ''
@@ -40,7 +40,7 @@ ${headers.join(',\n')}
   sharexElement.setAttribute('download', `${originClean}.sxcu`)
 }
 
-page.getPrettyDate = function (date) {
+page.getPrettyDate = date => {
   return date.getFullYear() + '-' +
     (date.getMonth() < 9 ? '0' : '') + // month's index starts from zero
     (date.getMonth() + 1) + '-' +
@@ -54,10 +54,10 @@ page.getPrettyDate = function (date) {
     date.getSeconds()
 }
 
-page.getPrettyBytes = function (num) {
+page.getPrettyBytes = num => {
   // MIT License
   // Copyright (c) Sindre Sorhus <[email protected]> (sindresorhus.com)
-  if (!Number.isFinite(num)) return num
+  if (typeof num !== 'number' && !isFinite(num)) return num
 
   const si = localStorage[lsKeys.siBytes] !== '0'
   const neg = num < 0 ? '-' : ''
@@ -65,7 +65,7 @@ page.getPrettyBytes = function (num) {
   if (neg) num = -num
   if (num < scale) return `${neg}${num} B`
 
-  const exponent = Math.min(Math.floor(Math.log10(num) / 3), 8) // 8 is count of KMGTPEZY
+  const exponent = Math.min(Math.floor((Math.log(num) * Math.LOG10E) / 3), 8) // 8 is count of KMGTPEZY
   const numStr = Number((num / Math.pow(scale, exponent)).toPrecision(3))
   const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')
   return `${neg}${numStr} ${pre}B`

+ 5 - 4
todo.md

@@ -7,12 +7,13 @@ Normal priority:
 * [ ] Use incremental version numbering instead of randomized strings.
 * [ ] Use versioning in APIs, somehow.
 * [ ] Better `df` handling (system disk stats).
-* [ ] Use loading spinners on dashboard's sidebar menus.
-* [ ] Disable all other sidebar menus when a menu is still loading.
+* [*] Use loading spinners on dashboard's sidebar menus.
+* [*] Disable all other sidebar menus when a menu is still loading.
 * [ ] Collapsible dashboard's sidebar albums menus.
-* [ ] Change `title` attribute of disabled control buttons in uploads & users lists.
-* [ ] Use Gatsby logo for link to [blog.fiery.me](https://blog.fiery.me/) on the homepage.
+* [*] Change `title` attribute of disabled control buttons in uploads & users lists.
+* [*] Use Gatsby logo for link to [blog.fiery.me](https://blog.fiery.me/) on the homepage.
 * [ ] Auto-detect missing columns in `database/db.js`.
+* [*] Better error message when server is down.
 
 Low priority:
 

+ 6 - 6
views/_globals.njk

@@ -16,9 +16,9 @@
   v3: CSS and JS files (libs such as bulma, lazyload, etc).
   v4: Renders in /public/render/* directories (to be used by render.js).
 #}
-{% set v1 = "gI6ZM0Tg0t" %}
+{% set v1 = "fFS2CGH95j" %}
 {% set v2 = "hiboQUzAzp" %}
-{% set v3 = "tWLiAlAX5i" %}
+{% set v3 = "fFS2CGH95j" %}
 {% set v4 = "S3TAWpPeFS" %}
 
 {#
@@ -38,14 +38,14 @@
   },
   {
     attrs: {
-      title: 'Blog',
+      title: 'Blog (Gatsby)',
       href: 'https://blog.fiery.me'
     },
-    icon: 'icon-archive icon-2x'
+    icon: 'icon-gatsby icon-2x'
   },
   {
     attrs: {
-      title: 'PrivateBin',
+      title: 'Paste (PrivateBin)',
       href: 'https://paste.fiery.me'
     },
     icon: 'icon-privatebin icon-2x'
@@ -53,7 +53,7 @@
   {
     attrs: {
       id: 'ShareX',
-      title: 'ShareX',
+      title: 'ShareX user profile',
       href: 'https://safe.fiery.me/safe.fiery.me.sxcu?v=' + v2
     },
     icon: 'icon-sharex icon-2x'

+ 2 - 2
views/album.njk

@@ -13,7 +13,7 @@
 <!-- Scripts -->
 <script src="../libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
 <script src="../js/album.js?v={{ globals.v1 }}"></script>
-<script src="../js/s/utils.js?v={{ globals.v1 }}"></script>
+<script src="../js/misc/utils.js?v={{ globals.v1 }}"></script>
 {% endif %}
 {% endblock %}
 
@@ -83,7 +83,7 @@
     {% if files.length -%}
     <div id="table" class="columns is-multiline is-mobile is-centered has-text-centered">
       {% for file in files %}
-        <div class="image-container column is-narrow">
+        <div class="image-container column is-narrow is-relative">
           <a class="image" href="{{ file.file }}" target="_blank" rel="noopener">
             {% if file.thumb -%}
               {% if nojs -%}

+ 1 - 2
views/auth.njk

@@ -4,7 +4,6 @@
 {{ super() }}
 <link rel="stylesheet" href="libs/fontello/fontello.css?v={{ globals.v3 }}">
 <link rel="stylesheet" href="css/sweetalert.css?v={{ globals.v1 }}">
-<link rel="stylesheet" href="css/auth.css?v={{ globals.v1 }}">
 {% endblock %}
 
 {% block scripts %}
@@ -53,7 +52,7 @@
                 </button>
               </div>
               <div class="control">
-                <button id="loginBtn" type="submit" class="button">
+                <button id="loginBtn" type="submit" class="button is-info">
                   <span class="icon">
                     <i class="icon-login"></i>
                   </span>

+ 16 - 16
views/dashboard.njk

@@ -15,7 +15,7 @@
 <script src="libs/clipboard.js/clipboard.min.js?v={{ globals.v3 }}"></script>
 <script src="libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
 <script src="js/dashboard.js?v={{ globals.v1 }}"></script>
-<script src="js/s/utils.js?v={{ globals.v1 }}"></script>
+<script src="js/misc/utils.js?v={{ globals.v1 }}"></script>
 {% endblock %}
 
 {% block content %}
@@ -34,51 +34,51 @@
       <div class="column is-one-quarter">
         <aside id="menu" class="menu">
           <p class="menu-label">General</p>
-          <ul class="menu-list">
+          <ul class="menu-list is-unselectable">
             <li>
-              <a href=".">Frontpage</a>
+              <a href="." class="is-relative">Frontpage</a>
             </li>
             <li>
-              <a id="itemUploads">Uploads</a>
+              <a id="itemUploads" class="is-relative">Uploads</a>
             </li>
             <li>
-              <a id="itemDeleteUploadsByNames">Delete uploads by names</a>
+              <a id="itemDeleteUploadsByNames" class="is-relative">Delete uploads by names</a>
             </li>
           </ul>
           <p class="menu-label">Albums</p>
-          <ul class="menu-list">
+          <ul class="menu-list is-unselectable">
             <li>
-              <a id="itemManageAlbums">Manage your albums</a>
+              <a id="itemManageAlbums" class="is-relative">Manage your albums</a>
             </li>
             <li>
               <ul id="albumsContainer"></ul>
             </li>
           </ul>
           <p id="itemLabelAdmin" class="menu-label is-hidden">Administration</p>
-          <ul id="itemListAdmin" class="menu-list is-hidden">
+          <ul id="itemListAdmin" class="menu-list is-unselectable is-hidden">
             <li>
-              <a id="itemStatistics" class="is-hidden">Statistics</a>
+              <a id="itemStatistics" class="is-relative is-hidden">Statistics</a>
             </li>
             <li>
-              <a id="itemManageUploads" class="is-hidden">Manage uploads</a>
+              <a id="itemManageUploads" class="is-relative is-hidden">Manage uploads</a>
             </li>
             <li>
-              <a id="itemManageUsers" class="is-hidden">Manage users</a>
+              <a id="itemManageUsers" class="is-relative is-hidden">Manage users</a>
             </li>
           </ul>
           <p class="menu-label">Configuration</p>
-          <ul class="menu-list">
+          <ul class="menu-list is-unselectable">
             <li>
-              <a id="ShareX">ShareX user profile</a>
+              <a id="ShareX" class="is-relative">ShareX user profile</a>
             </li>
             <li>
-              <a id="itemManageToken">Manage your token</a>
+              <a id="itemManageToken" class="is-relative">Manage your token</a>
             </li>
             <li>
-              <a id="itemChangePassword">Change your password</a>
+              <a id="itemChangePassword" class="is-relative">Change your password</a>
             </li>
             <li>
-              <a id="itemLogout">Logout</a>
+              <a id="itemLogout" class="is-relative">Logout</a>
             </li>
           </ul>
         </aside>

+ 13 - 9
views/home.njk

@@ -22,9 +22,9 @@
 <script src="libs/clipboard.js/clipboard.min.js?v={{ globals.v3 }}"></script>
 <script src="libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
 <script src="js/home.js?v={{ globals.v1 }}"></script>
-<script src="js/s/utils.js?v={{ globals.v1 }}"></script>
+<script src="js/misc/utils.js?v={{ globals.v1 }}"></script>
 {# We assign an ID for this so that the script can find out version string for render images #}
-<script id="renderScript" data-version="{{ globals.v4 }}" src="js/s/render.js?v={{ globals.v1 }}"></script>
+<script id="renderScript" data-version="{{ globals.v4 }}" src="js/misc/render.js?v={{ globals.v1 }}"></script>
 {% endblock %}
 
 {% block content %}
@@ -32,13 +32,15 @@
 <section id="home" class="hero is-fullheight">
   <div class="hero-body">
     <div class="container has-text-centered">
-      <p id="b">
+      <p id="b" class="is-relative">
         <img class="logo" alt="logo" src="images/logo_smol.png?v={{ globals.v2 }}">
       </p>
       <h1 class="title">{{ globals.name }}</h1>
       <h2 class="subtitle">{{ globals.home_subtitle | safe }}</h2>
 
-      <h3 id="maxSize" class="subtitle"></h3>
+      <h3 id="maxSize" class="subtitle">
+        Maximum upload size per file is <span>{{ maxSize }} MB</span>
+      </h3>
 
       <div class="columns is-gapless">
         <div class="column is-hidden-mobile"></div>
@@ -47,11 +49,13 @@
           <div id="albumDiv" class="field has-addons is-hidden">
             <div class="control is-expanded">
               <div class="select is-fullwidth">
-                <select id="albumSelect"></select>
+                <select id="albumSelect">
+                  <option value="" selected>Upload to album</option>
+                </select>
               </div>
             </div>
             <div class="control">
-              <a id="createAlbum" class="button is-breeze" title="Create new album">
+              <a id="createAlbum" class="button is-info" title="Create new album">
                 <i class="icon-plus"></i>
               </a>
             </div>
@@ -92,7 +96,7 @@
               </div>
               <p class="help">
                 {% if urlMaxSize !== maxSize -%}
-                Maximum file size per URL is <span id="urlMaxSize">{{ urlMaxSize }}</span>.
+                Maximum file size per URL is <span id="urlMaxSize">{{ urlMaxSize }} MB</span>.
                 {%- endif %}
 
                 {% if urlExtensionsFilter.length and (urlExtensionsFilterMode === 'blacklist') -%}
@@ -197,9 +201,9 @@
           </p>
           <p class="help expiry-date is-hidden"></p>
           <p class="clipboard-mobile is-hidden">
-            <a class="button is-small is-info is-outlined clipboard-js" style="display: flex">
+            <a class="button is-small is-info is-outlined is-flex clipboard-js">
               <span class="icon">
-                <i class="icon-clipboard-1"></i>
+                <i class="icon-clipboard"></i>
               </span>
               <span>Copy link to clipboard</span>
             </a>

+ 13 - 7
views/nojs.njk

@@ -23,18 +23,24 @@
       <div class="columns is-gapless">
         <div class="column is-hidden-mobile"></div>
         <div class="column">
-          <p class="subtitle" style="font-size: 1rem">
-            Files uploaded through this No-JS uploader will not be associated to your account, if you have any.
-          </p>
           {% if renderOptions.uploadDisabled -%}
-          <a class="button is-danger" style="display: flex" href="auth">{{ renderOptions.uploadDisabled }}</a>
+          <a class="button is-danger is-flex" href="auth">
+            {{ renderOptions.uploadDisabled }}
+          </a>
           {%- else -%}
-          <form id="form" class="field" action="" method="post" enctype="multipart/form-data">
+          <form id="form" class="field" action="?no-cache" method="post" enctype="multipart/form-data">
             <div class="field">
-              <input type="file" name="files[]" multiple="multiple" style="width: 100%">
+              <p class="control is-expanded">
+                <input type="file" name="files[]" multiple="multiple">
+              </p>
             </div>
             <div class="field">
-              <input type="submit" class="button is-danger" value="Upload" style="width: 100%">
+              <p class="control is-expanded">
+                <input type="submit" class="button is-danger is-fullwidth" value="Upload">
+              </p>
+              <p class="help">
+                Files uploaded through this form will not be associated with your account, if you have any.
+              </p>
             </div>
           </form>
           {%- endif %}

File diff suppressed because it is too large
+ 2737 - 97
yarn.lock