Merge pull request #278 from Zephyrrus/Zephyrrus-feature/database_based_settings
Zephyrrus feature/database based settings
This commit is contained in:
commit
065c5221a0
|
@ -13,3 +13,4 @@ database.sqlite-journal
|
||||||
docker/nginx/chibisafe.moe.conf
|
docker/nginx/chibisafe.moe.conf
|
||||||
docker-compose.config.yml
|
docker-compose.config.yml
|
||||||
/coverage
|
/coverage
|
||||||
|
local/
|
||||||
|
|
|
@ -7,18 +7,16 @@ services:
|
||||||
SECRET: "wowfcgMHqZHwOIMLadWrKu3liyqPOOILpDLSDvuxq3YGhJmiZXJCVpnF96l11WfR"
|
SECRET: "wowfcgMHqZHwOIMLadWrKu3liyqPOOILpDLSDvuxq3YGhJmiZXJCVpnF96l11WfR"
|
||||||
ADMIN_ACCOUNT: "admin"
|
ADMIN_ACCOUNT: "admin"
|
||||||
ADMIN_PASSWORD: "admin"
|
ADMIN_PASSWORD: "admin"
|
||||||
|
# OVERWRITE_SETTINGS: 'false'
|
||||||
# ROUTE_PREFIX: /api
|
# ROUTE_PREFIX: /api
|
||||||
# RATE_LIMIT_WINDOW: 2
|
# RATE_LIMIT_WINDOW: 2
|
||||||
# RATE_LIMIT_MAX: 5
|
# RATE_LIMIT_MAX: 5
|
||||||
# BLOCKED_EXTENSIONS: '.jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh'
|
# BLOCKED_EXTENSIONS: '.jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh'
|
||||||
# UPLOAD_FOLDER: uploads
|
|
||||||
# MAX_LINKS_PER_ALBUM: 5
|
|
||||||
# META_THEME_COLOR: '#20222b'
|
# META_THEME_COLOR: '#20222b'
|
||||||
# META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀'
|
# META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀'
|
||||||
# META_KEYWORDS: 'chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free'
|
# META_KEYWORDS: 'chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free'
|
||||||
# META_TWITTER_HANDLE: ''
|
# META_TWITTER_HANDLE: ''
|
||||||
# SERVER_PORT: 5000
|
# SERVER_PORT: 5000
|
||||||
# WEBSITE_PORT: 5001
|
|
||||||
# DOMAIN: 'http://chibisafe.moe'
|
# DOMAIN: 'http://chibisafe.moe'
|
||||||
# SERVICE_NAME: chibisafe
|
# SERVICE_NAME: chibisafe
|
||||||
# MAX_SIZE: 5000
|
# MAX_SIZE: 5000
|
||||||
|
|
|
@ -26,12 +26,12 @@ services:
|
||||||
- "5001"
|
- "5001"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
OVERWRITE_SETTINGS: "false"
|
||||||
CHUNK_SIZE: 90
|
CHUNK_SIZE: 90
|
||||||
ROUTE_PREFIX: /api
|
ROUTE_PREFIX: /api
|
||||||
RATE_LIMIT_WINDOW: 2
|
RATE_LIMIT_WINDOW: 2
|
||||||
RATE_LIMIT_MAX: 5
|
RATE_LIMIT_MAX: 5
|
||||||
BLOCKED_EXTENSIONS: ".jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh"
|
BLOCKED_EXTENSIONS: ".jar,.exe,.msi,.com,.bat,.cmd,.scr,.ps1,.sh"
|
||||||
UPLOAD_FOLDER: uploads
|
|
||||||
SECRET: ""
|
SECRET: ""
|
||||||
MAX_LINKS_PER_ALBUM: 5
|
MAX_LINKS_PER_ALBUM: 5
|
||||||
META_THEME_COLOR: "#20222b"
|
META_THEME_COLOR: "#20222b"
|
||||||
|
@ -39,8 +39,7 @@ services:
|
||||||
META_KEYWORDS: "chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free"
|
META_KEYWORDS: "chibisafe,upload,uploader,file,vue,images,ssr,file uploader,free"
|
||||||
META_TWITTER_HANDLE: ""
|
META_TWITTER_HANDLE: ""
|
||||||
SERVER_PORT: 5000
|
SERVER_PORT: 5000
|
||||||
WEBSITE_PORT: 5001
|
DOMAIN: "http://localhost:5000"
|
||||||
DOMAIN: "http://chibisafe.moe"
|
|
||||||
SERVICE_NAME: chibisafe
|
SERVICE_NAME: chibisafe
|
||||||
MAX_SIZE: 5000
|
MAX_SIZE: 5000
|
||||||
GENERATE_THUMBNAILS: "true"
|
GENERATE_THUMBNAILS: "true"
|
||||||
|
|
|
@ -6,7 +6,7 @@ For starters we recommend cloning the new version somewhere else instead of `git
|
||||||
- Then copy your `database/db` file from your v3 folder to the root of your v4 folder.
|
- Then copy your `database/db` file from your v3 folder to the root of your v4 folder.
|
||||||
- Make sure to install the dependencies by running `npm i`
|
- Make sure to install the dependencies by running `npm i`
|
||||||
- You then need to run `npm run setup` from the v4 folder and finish the setup process.
|
- You then need to run `npm run setup` from the v4 folder and finish the setup process.
|
||||||
- Once that's done you need to manually run `node src/api/databaseMigration.js` from the root folder of v4.
|
- Once that's done you need to manually run `node src/api/scripts/databaseMigration.js` from the root folder of v4.
|
||||||
- This will migrate the v3 database to v4 and regenerate every single thumbnail in webp to save bandwidth.
|
- This will migrate the v3 database to v4 and regenerate every single thumbnail in webp to save bandwidth.
|
||||||
- After the migration finishes, the last step is to update your nginx config with the [newly provided script](./nginx.md).
|
- After the migration finishes, the last step is to update your nginx config with the [newly provided script](./nginx.md).
|
||||||
- Restart nginx with `sudo nginx -s reload`.
|
- Restart nginx with `sudo nginx -s reload`.
|
||||||
|
|
|
@ -1,61 +1,46 @@
|
||||||
import dotenv from 'dotenv/config';
|
import dotenv from 'dotenv/config';
|
||||||
import autoprefixer from 'autoprefixer';
|
import autoprefixer from 'autoprefixer';
|
||||||
import jetpack from 'fs-jetpack';
|
|
||||||
|
|
||||||
const clientConfig = {
|
const Util = require('./src/api/utils/Util');
|
||||||
development: process.env.NODE_ENV !== 'production',
|
|
||||||
version: process.env.npm_package_version,
|
|
||||||
URL: process.env.DOMAIN,
|
|
||||||
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`,
|
|
||||||
serviceName: process.env.SERVICE_NAME,
|
|
||||||
maxFileSize: parseInt(process.env.MAX_SIZE, 10),
|
|
||||||
chunkSize: parseInt(process.env.CHUNK_SIZE, 10),
|
|
||||||
maxLinksPerAlbum: parseInt(process.env.MAX_LINKS_PER_ALBUM, 10),
|
|
||||||
publicMode: process.env.PUBLIC_MODE === 'true',
|
|
||||||
userAccounts: process.env.USER_ACCOUNTS === 'true'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
server: {
|
|
||||||
port: process.env.WEBSITE_PORT
|
|
||||||
},
|
|
||||||
srcDir: 'src/site/',
|
srcDir: 'src/site/',
|
||||||
head: {
|
head: {
|
||||||
title: process.env.SERVICE_NAME,
|
title: Util.config.serviceName,
|
||||||
titleTemplate: `%s | ${process.env.SERVICE_NAME}`,
|
titleTemplate: `%s | ${Util.config.serviceName}`,
|
||||||
// TODO: Add the directory with pictures for favicon and stuff
|
// TODO: Add the directory with pictures for favicon and stuff
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
{ hid: 'theme-color', name: 'theme-color', content: `${process.env.META_THEME_COLOR}` },
|
{ hid: 'theme-color', name: 'theme-color', content: `${Util.config.metaThemeColor}` },
|
||||||
{ hid: 'description', name: 'description', content: `${process.env.META_DESCRIPTION}` },
|
{ hid: 'description', name: 'description', content: `${Util.config.metaDescription}` },
|
||||||
{ hid: 'keywords', name: 'keywords', content: `${process.env.META_KEYWORDS}` },
|
{ hid: 'keywords', name: 'keywords', content: `${Util.config.metaKeywords}` },
|
||||||
{
|
{
|
||||||
hid: 'apple-mobile-web-app-title',
|
hid: 'apple-mobile-web-app-title',
|
||||||
name: 'apple-mobile-web-app-title',
|
name: 'apple-mobile-web-app-title',
|
||||||
content: `${process.env.SERVICE_NAME}`
|
content: `${Util.config.serviceName}`
|
||||||
},
|
},
|
||||||
{ hid: 'application-name', name: 'application-name', content: `${process.env.SERVICE_NAME}` },
|
{ hid: 'application-name', name: 'application-name', content: `${Util.config.serviceName}` },
|
||||||
{ hid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
{ hid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
||||||
{ hid: 'twitter:site', name: 'twitter:site', content: `${process.env.META_TWITTER_HANDLE}` },
|
{ hid: 'twitter:site', name: 'twitter:site', content: `${Util.config.metaTwitterHandle}` },
|
||||||
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${process.env.META_TWITTER_HANDLE}` },
|
{ hid: 'twitter:creator', name: 'twitter:creator', content: `${Util.config.metaTwitterHandle}` },
|
||||||
{ hid: 'twitter:title', name: 'twitter:title', content: `${process.env.SERVICE_NAME}` },
|
{ hid: 'twitter:title', name: 'twitter:title', content: `${Util.config.serviceName}` },
|
||||||
{ hid: 'twitter:description', name: 'twitter:description', content: `${process.env.META_DESCRIPTION}` },
|
{ hid: 'twitter:description', name: 'twitter:description', content: `${Util.config.metaDescription}` },
|
||||||
{ hid: 'twitter:image', name: 'twitter:image', content: `${process.env.DOMAIN}/logo.png` },
|
{ hid: 'twitter:image', name: 'twitter:image', content: `/logo.png` },
|
||||||
{ hid: 'og:url', property: 'og:url', content: `${process.env.DOMAIN}` },
|
{ hid: 'og:url', property: 'og:url', content: `/` },
|
||||||
{ hid: 'og:type', property: 'og:type', content: 'website' },
|
{ hid: 'og:type', property: 'og:type', content: 'website' },
|
||||||
{ hid: 'og:title', property: 'og:title', content: `${process.env.SERVICE_NAME}` },
|
{ hid: 'og:title', property: 'og:title', content: `${Util.config.serviceName}` },
|
||||||
{ hid: 'og:description', property: 'og:description', content: `${process.env.META_DESCRIPTION}` },
|
{ hid: 'og:description', property: 'og:description', content: `${Util.config.metaDescription}` },
|
||||||
{ hid: 'og:image', property: 'og:image', content: `${process.env.DOMAIN}/logo.png` },
|
{ hid: 'og:image', property: 'og:image', content: `/logo.png` },
|
||||||
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${process.env.DOMAIN}/logo.png` },
|
{ hid: 'og:image:secure_url', property: 'og:image:secure_url', content: `/logo.png` },
|
||||||
{ hid: 'og:site_name', property: 'og:site_name', content: `${process.env.SERVICE_NAME}` }
|
{ hid: 'og:site_name', property: 'og:site_name', content: `${Util.config.serviceName}` }
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' },
|
||||||
|
|
||||||
// This one is a pain in the ass to make it customizable, so you should edit it manually
|
// This one is a pain in the ass to make it customizable, so you should edit it manually
|
||||||
{ type: 'application/json+oembed', href: `${process.env.DOMAIN}/oembed.json` }
|
{ type: 'application/json+oembed', href: `/oembed.json` }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -74,8 +59,11 @@ export default {
|
||||||
linkActiveClass: 'is-active',
|
linkActiveClass: 'is-active',
|
||||||
linkExactActiveClass: 'is-active'
|
linkExactActiveClass: 'is-active'
|
||||||
},
|
},
|
||||||
|
env: {
|
||||||
|
development: process.env.NODE_ENV !== 'production'
|
||||||
|
},
|
||||||
axios: {
|
axios: {
|
||||||
baseURL: `${process.env.DOMAIN}${process.env.ROUTE_PREFIX}`
|
baseURL: `${process.env.NODE_ENV === 'production' ? process.env.DOMAIN : 'http://localhost:5000'}/api`
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
extractCSS: process.env.NODE_ENV === 'production',
|
extractCSS: process.env.NODE_ENV === 'production',
|
||||||
|
@ -84,13 +72,10 @@ export default {
|
||||||
autoprefixer
|
autoprefixer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extend(config, { isClient, isDev }) {
|
extend(config, { isDev }) {
|
||||||
// Extend only webpack config for client-bundle
|
// Extend only webpack config for client-bundle
|
||||||
if (isClient) {
|
|
||||||
jetpack.write('dist/config.json', clientConfig);
|
|
||||||
}
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
config.devtool = isClient ? 'source-map' : 'inline-source-map';
|
config.devtool = 'source-map';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1230,6 +1230,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@hapi/hoek": {
|
||||||
|
"version": "9.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz",
|
||||||
|
"integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw=="
|
||||||
|
},
|
||||||
|
"@hapi/topo": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==",
|
||||||
|
"requires": {
|
||||||
|
"@hapi/hoek": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@istanbuljs/load-nyc-config": {
|
"@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
|
@ -1893,6 +1906,117 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@mapbox/node-pre-gyp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
|
||||||
|
"requires": {
|
||||||
|
"detect-libc": "^1.0.3",
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"nopt": "^5.0.0",
|
||||||
|
"npmlog": "^4.1.2",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"semver": "^7.3.4",
|
||||||
|
"tar": "^6.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chownr": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https-proxy-agent": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||||
|
"requires": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"requires": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minizlib": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||||
|
"requires": {
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||||
|
},
|
||||||
|
"nopt": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||||
|
"requires": {
|
||||||
|
"abbrev": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tar": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||||
|
"requires": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@mdi/font": {
|
"@mdi/font": {
|
||||||
"version": "5.8.55",
|
"version": "5.8.55",
|
||||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.8.55.tgz",
|
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.8.55.tgz",
|
||||||
|
@ -3037,6 +3161,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@sideway/address": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==",
|
||||||
|
"requires": {
|
||||||
|
"@hapi/hoek": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@sideway/formula": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
|
||||||
|
},
|
||||||
|
"@sideway/pinpoint": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||||
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
|
||||||
|
@ -4412,18 +4554,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bcrypt": {
|
"bcrypt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
|
||||||
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
|
"integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-addon-api": "^3.0.0",
|
"@mapbox/node-pre-gyp": "^1.0.0",
|
||||||
"node-pre-gyp": "0.15.0"
|
"node-addon-api": "^3.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": {
|
"node-addon-api": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||||
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
|
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11566,6 +11708,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-0.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-0.1.20.tgz",
|
||||||
"integrity": "sha512-nlsuibooCG5yEjmGSVqxhjULy3rO1Gl0LDP+HpUMbzOSLcz5s1Gf5cPnjvHiei0JCG3SXX761HQArDzNIfdz4Q=="
|
"integrity": "sha512-nlsuibooCG5yEjmGSVqxhjULy3rO1Gl0LDP+HpUMbzOSLcz5s1Gf5cPnjvHiei0JCG3SXX761HQArDzNIfdz4Q=="
|
||||||
},
|
},
|
||||||
|
"joi": {
|
||||||
|
"version": "17.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.3.0.tgz",
|
||||||
|
"integrity": "sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==",
|
||||||
|
"requires": {
|
||||||
|
"@hapi/hoek": "^9.0.0",
|
||||||
|
"@hapi/topo": "^5.0.0",
|
||||||
|
"@sideway/address": "^4.1.0",
|
||||||
|
"@sideway/formula": "^3.0.0",
|
||||||
|
"@sideway/pinpoint": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"js-base64": {
|
"js-base64": {
|
||||||
"version": "2.6.4",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
|
||||||
|
@ -12847,30 +13001,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-1.4.2.tgz",
|
||||||
"integrity": "sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ=="
|
"integrity": "sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ=="
|
||||||
},
|
},
|
||||||
"node-pre-gyp": {
|
|
||||||
"version": "0.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz",
|
|
||||||
"integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==",
|
|
||||||
"requires": {
|
|
||||||
"detect-libc": "^1.0.2",
|
|
||||||
"mkdirp": "^0.5.3",
|
|
||||||
"needle": "^2.5.0",
|
|
||||||
"nopt": "^4.0.1",
|
|
||||||
"npm-packlist": "^1.1.6",
|
|
||||||
"npmlog": "^4.0.2",
|
|
||||||
"rc": "^1.2.7",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"semver": "^5.3.0",
|
|
||||||
"tar": "^4.4.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"semver": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-releases": {
|
"node-releases": {
|
||||||
"version": "1.1.66",
|
"version": "1.1.66",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
||||||
|
|
30
package.json
30
package.json
|
@ -9,19 +9,19 @@
|
||||||
"url": "https://github.com/Pitu"
|
"url": "https://github.com/Pitu"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node src/setup.js && npm run migrate && npm run seed && npm run build",
|
"setup": "node src/setup.js && npm run migrate && npm run seed",
|
||||||
"build": "nuxt build",
|
"start": "npm run migrate && nuxt build && cross-env NODE_ENV=production node src/api/structures/Server",
|
||||||
"start": "npm run migrate && cross-env NODE_ENV=production node src/api/structures/Server",
|
"dev": "nodemon src/api/structures/Server",
|
||||||
"dev": "nuxt",
|
|
||||||
"migrate": "knex migrate:latest",
|
"migrate": "knex migrate:latest",
|
||||||
"seed": "knex seed:run",
|
"seed": "knex seed:run",
|
||||||
"api": "node src/api/structures/Server",
|
|
||||||
"update": "git pull && npm install && npm run migrate && npm run build && npm run restart",
|
"update": "git pull && npm install && npm run migrate && npm run build && npm run restart",
|
||||||
"restart": "pm2 restart lolisafe",
|
"restart": "pm2 restart chibisafe",
|
||||||
|
"overwrite-config": "cross-env OVERWRITE_SETTINGS=true",
|
||||||
"test:vue": "jest --testPathPattern=src/site",
|
"test:vue": "jest --testPathPattern=src/site",
|
||||||
"test:api": "jest --testPathPattern=src/tests/api",
|
"test:api": "jest --testPathPattern=src/tests/api",
|
||||||
"test:e2e": "jest --testPathPattern=src/tests/e2e",
|
"test:e2e": "jest --testPathPattern=src/tests/e2e",
|
||||||
"tests": "npm run test:api && npm run test:vue && npm run test:e2e"
|
"tests": "npm run test:api && npm run test:vue && npm run test:e2e",
|
||||||
|
"sqlite": "sqlite_web -p 5001 database/database.sqlite"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
"@mdi/font": "^5.8.55",
|
"@mdi/font": "^5.8.55",
|
||||||
"@nuxtjs/axios": "^5.12.5",
|
"@nuxtjs/axios": "^5.12.5",
|
||||||
"adm-zip": "^0.4.13",
|
"adm-zip": "^0.4.13",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.1",
|
||||||
"blake3": "^2.1.4",
|
"blake3": "^2.1.4",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"buefy": "^0.9.4",
|
"buefy": "^0.9.4",
|
||||||
|
@ -59,6 +59,7 @@
|
||||||
"fs-jetpack": "^2.2.2",
|
"fs-jetpack": "^2.2.2",
|
||||||
"helmet": "^3.15.1",
|
"helmet": "^3.15.1",
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
|
"joi": "^17.3.0",
|
||||||
"jsonwebtoken": "^8.5.0",
|
"jsonwebtoken": "^8.5.0",
|
||||||
"knex": "^0.21.15",
|
"knex": "^0.21.15",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
|
@ -103,7 +104,7 @@
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-serializer-vue": "^2.0.2",
|
"jest-serializer-vue": "^2.0.2",
|
||||||
"node-sass": "^5.0.0",
|
"node-sass": "^5.0.0",
|
||||||
"nodemon": "^1.19.3",
|
"nodemon": "^1.19.4",
|
||||||
"postcss-css-variables": "^0.11.0",
|
"postcss-css-variables": "^0.11.0",
|
||||||
"postcss-nested": "^3.0.0",
|
"postcss-nested": "^3.0.0",
|
||||||
"puppeteer": "^5.5.0",
|
"puppeteer": "^5.5.0",
|
||||||
|
@ -131,6 +132,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodemonConfig": {
|
||||||
|
"watch": [
|
||||||
|
"src/api/*"
|
||||||
|
],
|
||||||
|
"delay": 2500
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"chibisafe",
|
"chibisafe",
|
||||||
"lolisafe",
|
"lolisafe",
|
||||||
|
@ -141,5 +148,8 @@
|
||||||
"ssr",
|
"ssr",
|
||||||
"file uploader",
|
"file uploader",
|
||||||
"images"
|
"images"
|
||||||
]
|
],
|
||||||
|
"volta": {
|
||||||
|
"node": "14.17.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
|
exports.up = async knex => {
|
||||||
|
await knex.schema.createTable('settings', table => {
|
||||||
|
table.string('key');
|
||||||
|
table.string('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const defaults = Util.getEnvironmentDefaults();
|
||||||
|
const keys = Object.keys(defaults);
|
||||||
|
for (const item of keys) {
|
||||||
|
await knex('settings').insert({
|
||||||
|
key: item,
|
||||||
|
value: JSON.stringify(defaults[item])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async knex => {
|
||||||
|
await knex.schema.dropTableIfExists('settings');
|
||||||
|
};
|
|
@ -1,15 +1,41 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
exports.seed = async db => {
|
exports.seed = async db => {
|
||||||
const now = moment.utc().toDate();
|
const now = moment.utc().toDate();
|
||||||
const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first();
|
|
||||||
if (user) return;
|
// Save environment variables to the database
|
||||||
try {
|
try {
|
||||||
const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
|
const defaults = Util.getEnvironmentDefaults();
|
||||||
|
const keys = Object.keys(defaults);
|
||||||
|
for (const item of keys) {
|
||||||
|
await Util.writeConfigToDb({
|
||||||
|
key: item,
|
||||||
|
value: defaults[item]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user if it doesnt exist
|
||||||
|
const user = await db.table('users').where({ username: 'admin' }).first();
|
||||||
|
if (user) {
|
||||||
|
console.log();
|
||||||
|
console.log('=========================================================');
|
||||||
|
console.log('== admin account already exists, skipping. ==');
|
||||||
|
console.log('=========================================================');
|
||||||
|
console.log('== Run `pm2 start pm2.json` to start the service ==');
|
||||||
|
console.log('=========================================================');
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const hash = await bcrypt.hash('admin', 10);
|
||||||
await db.table('users').insert({
|
await db.table('users').insert({
|
||||||
username: process.env.ADMIN_ACCOUNT,
|
username: 'admin',
|
||||||
password: hash,
|
password: hash,
|
||||||
passwordEditedAt: now,
|
passwordEditedAt: now,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
|
@ -15,7 +15,7 @@ class filesGET extends Route {
|
||||||
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
.select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin')
|
||||||
.where({ id: file.userId })
|
.where({ id: file.userId })
|
||||||
.first();
|
.first();
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
|
|
||||||
// Additional relevant data
|
// Additional relevant data
|
||||||
const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
|
const filesFromUser = await db.table('files').where({ userId: user.id }).select('id');
|
||||||
|
|
|
@ -37,7 +37,7 @@ class usersGET extends Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -43,7 +43,7 @@ class albumGET extends Route {
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -44,7 +44,7 @@ class albumGET extends Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 1 more view to the link
|
// Add 1 more view to the link
|
||||||
|
|
|
@ -38,13 +38,13 @@ class albumGET extends Route {
|
||||||
If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user
|
If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user
|
||||||
*/
|
*/
|
||||||
if (album.zippedAt > album.editedAt) {
|
if (album.zippedAt > album.editedAt) {
|
||||||
const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`);
|
const filePath = path.join(__dirname, '../../../../uploads', 'zips', `${album.userId}-${album.id}.zip`);
|
||||||
const exists = await jetpack.existsAsync(filePath);
|
const exists = await jetpack.existsAsync(filePath);
|
||||||
/*
|
/*
|
||||||
Make sure the file exists just in case, and if not, continue to it's generation.
|
Make sure the file exists just in case, and if not, continue to it's generation.
|
||||||
*/
|
*/
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`;
|
const fileName = `${Util.config.serviceName}-${identifier}.zip`;
|
||||||
return res.download(filePath, fileName);
|
return res.download(filePath, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,8 +77,8 @@ class albumGET extends Route {
|
||||||
.update('zippedAt', db.fn.now())
|
.update('zippedAt', db.fn.now())
|
||||||
.wasMutated();
|
.wasMutated();
|
||||||
|
|
||||||
const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`);
|
const filePath = path.join(__dirname, '../../../../uploads', 'zips', `${album.userId}-${album.id}.zip`);
|
||||||
const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`;
|
const fileName = `${Util.config.serviceName}-${identifier}.zip`;
|
||||||
return res.download(filePath, fileName);
|
return res.download(filePath, fileName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
|
|
|
@ -37,7 +37,7 @@ class albumsGET extends Route {
|
||||||
|
|
||||||
// Fetch thumbnails and stuff
|
// Fetch thumbnails and stuff
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
album.fileCount = fileCount[0].count;
|
album.fileCount = fileCount[0].count;
|
||||||
|
|
|
@ -20,16 +20,6 @@ class linkPOST extends Route {
|
||||||
.first();
|
.first();
|
||||||
if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
|
if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
|
||||||
|
|
||||||
/*
|
|
||||||
Count the amount of links created for that album already and error out if max was reached
|
|
||||||
*/
|
|
||||||
const count = await db
|
|
||||||
.table('links')
|
|
||||||
.where('albumId', albumId)
|
|
||||||
.count({ count: 'id' })
|
|
||||||
.first();
|
|
||||||
if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' });
|
|
||||||
|
|
||||||
let { identifier } = req.body;
|
let { identifier } = req.body;
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
if (!user.isAdmin) return res.status(401).json({ message: 'Only administrators can create custom links' });
|
if (!user.isAdmin) return res.status(401).json({ message: 'Only administrators can create custom links' });
|
||||||
|
|
|
@ -2,6 +2,7 @@ const bcrypt = require('bcrypt');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const JWT = require('jsonwebtoken');
|
const JWT = require('jsonwebtoken');
|
||||||
const Route = require('../../structures/Route');
|
const Route = require('../../structures/Route');
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
class loginPOST extends Route {
|
class loginPOST extends Route {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -37,7 +38,7 @@ class loginPOST extends Route {
|
||||||
iss: 'chibisafe',
|
iss: 'chibisafe',
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
iat: moment.utc().valueOf()
|
iat: moment.utc().valueOf()
|
||||||
}, process.env.SECRET, { expiresIn: '30d' });
|
}, Util.config.secret, { expiresIn: '30d' });
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
message: 'Successfully logged in.',
|
message: 'Successfully logged in.',
|
||||||
|
|
|
@ -12,7 +12,7 @@ class registerPOST extends Route {
|
||||||
async run(req, res, db) {
|
async run(req, res, db) {
|
||||||
// Only allow admins to create new accounts if the sign up is deactivated
|
// Only allow admins to create new accounts if the sign up is deactivated
|
||||||
const user = await Util.isAuthorized(req);
|
const user = await Util.isAuthorized(req);
|
||||||
if ((!user || !user.isAdmin) && process.env.USER_ACCOUNTS === 'false') return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
|
if ((!user || !user.isAdmin) && !Util.config.userAccounts) return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
|
||||||
|
|
||||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
|
@ -16,7 +16,7 @@ class fileGET extends Route {
|
||||||
let file = await db.table('files').where({ id, userId: user.id }).first();
|
let file = await db.table('files').where({ id, userId: user.id }).first();
|
||||||
if (!file) return res.status(400).json({ message: 'The file doesn\'t exist or doesn\'t belong to the user' });
|
if (!file) return res.status(400).json({ message: 'The file doesn\'t exist or doesn\'t belong to the user' });
|
||||||
|
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Fetch the albums
|
Fetch the albums
|
||||||
|
|
|
@ -30,7 +30,7 @@ class filesGET extends Route {
|
||||||
|
|
||||||
// For each file, create the public link to be able to display the file
|
// For each file, create the public link to be able to display the file
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -53,7 +53,7 @@ class configGET extends Route {
|
||||||
|
|
||||||
// For each file, create the public link to be able to display the file
|
// For each file, create the public link to be able to display the file
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
file = Util.constructFilePublicLink(file);
|
file = Util.constructFilePublicLink(req, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
const Route = require('../../structures/Route');
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
|
class configGET extends Route {
|
||||||
|
constructor() {
|
||||||
|
super('/service/config/all', 'get', { adminOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
run(req, res) {
|
||||||
|
return res.json({
|
||||||
|
message: 'Successfully retrieved config',
|
||||||
|
config: Util.config
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = configGET;
|
|
@ -1,24 +1,23 @@
|
||||||
const Route = require('../../structures/Route');
|
const Route = require('../../structures/Route');
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
class configGET extends Route {
|
class configGET extends Route {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('/service/config', 'get', { adminOnly: true });
|
super('/service/config', 'get', { bypassAuth: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
run(req, res) {
|
run(req, res) {
|
||||||
return res.json({
|
return res.json({
|
||||||
message: 'Successfully retrieved config',
|
message: 'Successfully retrieved config',
|
||||||
config: {
|
config: {
|
||||||
serviceName: process.env.SERVICE_NAME,
|
version: process.env.npm_package_version,
|
||||||
uploadFolder: process.env.UPLOAD_FOLDER,
|
serviceName: Util.config.serviceName,
|
||||||
linksPerAlbum: parseInt(process.env.MAX_LINKS_PER_ALBUM, 10),
|
maxUploadSize: Util.config.maxSize,
|
||||||
maxUploadSize: parseInt(process.env.MAX_SIZE, 10),
|
filenameLength: Util.config.generatedFilenameLength,
|
||||||
filenameLength: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10),
|
albumLinkLength: Util.config.generatedAlbumLength,
|
||||||
albumLinkLength: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10),
|
chunkSize: Util.config.chunkSize,
|
||||||
generateThumbnails: process.env.GENERATE_THUMBNAILS === 'true',
|
publicMode: Util.config.publicMode,
|
||||||
generateZips: process.env.GENERATE_ZIPS === 'true',
|
userAccounts: Util.config.userAccounts
|
||||||
publicMode: process.env.PUBLIC_MODE === 'true',
|
|
||||||
enableAccounts: process.env.USER_ACCOUNTS === 'true'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
const Route = require('../../structures/Route');
|
||||||
|
const Util = require('../../utils/Util');
|
||||||
|
|
||||||
|
const { schema } = require('../../structures/Setting');
|
||||||
|
|
||||||
|
const joiOptions = {
|
||||||
|
abortEarly: false, // include all errors
|
||||||
|
allowUnknown: true, // ignore unknown props
|
||||||
|
stripUnknown: true // remove unknown props
|
||||||
|
};
|
||||||
|
|
||||||
|
class configGET extends Route {
|
||||||
|
constructor() {
|
||||||
|
super('/service/config', 'post', { adminOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(req, res) {
|
||||||
|
const { settings } = req.body;
|
||||||
|
const { error, value } = schema.validate(settings, joiOptions);
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: error.details.reduce((acc, v) => {
|
||||||
|
for (const p of v.path) {
|
||||||
|
acc[p] = (acc[p] || []).concat(v.message);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Util.wipeConfigDb();
|
||||||
|
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
for await (const item of keys) {
|
||||||
|
Util.writeConfigToDb({
|
||||||
|
key: item,
|
||||||
|
value: value[item]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = configGET;
|
|
@ -0,0 +1,17 @@
|
||||||
|
const Route = require('../../structures/Route');
|
||||||
|
const { configSchema } = require('../../structures/Setting');
|
||||||
|
|
||||||
|
class configGET extends Route {
|
||||||
|
constructor() {
|
||||||
|
super('/service/config/schema', 'get', { adminOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
run(req, res) {
|
||||||
|
return res.json({
|
||||||
|
message: 'Successfully retrieved schema',
|
||||||
|
schema: configSchema
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = configGET;
|
|
@ -8,8 +8,8 @@ const multerStorage = require('../../utils/multerStorage');
|
||||||
|
|
||||||
const chunksData = {};
|
const chunksData = {};
|
||||||
const chunkedUploadsTimeout = 1800000;
|
const chunkedUploadsTimeout = 1800000;
|
||||||
const chunksDir = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'chunks');
|
const chunksDir = path.join(__dirname, '../../../../uploads/chunks');
|
||||||
const uploadDir = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER);
|
const uploadDir = path.join(__dirname, '../../../../uploads');
|
||||||
|
|
||||||
|
|
||||||
const cleanUpChunks = async (uuid, onTimeout) => {
|
const cleanUpChunks = async (uuid, onTimeout) => {
|
||||||
|
@ -72,7 +72,7 @@ const initChunks = async uuid => {
|
||||||
const executeMulter = multer({
|
const executeMulter = multer({
|
||||||
// Guide: https://github.com/expressjs/multer#limits
|
// Guide: https://github.com/expressjs/multer#limits
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000),
|
fileSize: Util.config.maxSize * (1000 * 1000),
|
||||||
// Maximum number of non-file fields.
|
// Maximum number of non-file fields.
|
||||||
// Dropzone.js will add 6 extra fields for chunked uploads.
|
// Dropzone.js will add 6 extra fields for chunked uploads.
|
||||||
// We don't use them for anything else.
|
// We don't use them for anything else.
|
||||||
|
@ -257,7 +257,7 @@ class uploadPOST extends Route {
|
||||||
|
|
||||||
async run(req, res, db) {
|
async run(req, res, db) {
|
||||||
const user = await Util.isAuthorized(req);
|
const user = await Util.isAuthorized(req);
|
||||||
if (!user && process.env.PUBLIC_MODE === 'false') return res.status(401).json({ message: 'Not authorized to use this resource' });
|
if (!user && !Util.config.publicMode) return res.status(401).json({ message: 'Not authorized to use this resource' });
|
||||||
const { finishedchunks } = req.headers;
|
const { finishedchunks } = req.headers;
|
||||||
const albumId = req.headers.albumid ? req.headers.albumid === 'null' ? null : req.headers.albumid : null;
|
const albumId = req.headers.albumid ? req.headers.albumid === 'null' ? null : req.headers.albumid : null;
|
||||||
if (albumId && !user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
|
if (albumId && !user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
|
||||||
|
@ -282,8 +282,8 @@ class uploadPOST extends Route {
|
||||||
|
|
||||||
if (albumId) await Util.saveFileToAlbum(db, albumId, result.id);
|
if (albumId) await Util.saveFileToAlbum(db, albumId, result.id);
|
||||||
|
|
||||||
result.file = Util.constructFilePublicLink(result.file);
|
result.file = Util.constructFilePublicLink(req, result.file);
|
||||||
result.deleteUrl = `${process.env.DOMAIN}/api/file/${result.id[0]}`;
|
result.deleteUrl = `${Util.getHost(req)}/api/file/${result.id[0]}`;
|
||||||
|
|
||||||
return res.status(201).send({
|
return res.status(201).send({
|
||||||
message: 'Sucessfully uploaded the file.',
|
message: 'Sucessfully uploaded the file.',
|
||||||
|
|
|
@ -3,7 +3,7 @@ require('dotenv').config();
|
||||||
const nodePath = require('path');
|
const nodePath = require('path');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const jetpack = require('fs-jetpack');
|
const jetpack = require('fs-jetpack');
|
||||||
const ThumbUtil = require('./utils/ThumbUtil');
|
const ThumbUtil = require('../utils/ThumbUtil');
|
||||||
|
|
||||||
const oldDb = require('knex')({
|
const oldDb = require('knex')({
|
||||||
client: 'sqlite3',
|
client: 'sqlite3',
|
||||||
|
@ -19,12 +19,7 @@ const newDb = require('knex')({
|
||||||
filename: nodePath.join(__dirname, '../../database/', 'database.sqlite')
|
filename: nodePath.join(__dirname, '../../database/', 'database.sqlite')
|
||||||
},
|
},
|
||||||
postProcessResponse: result => {
|
postProcessResponse: result => {
|
||||||
const booleanFields = [
|
const booleanFields = ['enabled', 'enableDownload', 'isAdmin', 'nsfw', 'generateZips', 'publicMode', 'userAccounts'];
|
||||||
'enabled',
|
|
||||||
'enableDownload',
|
|
||||||
'isAdmin',
|
|
||||||
'nsfw'
|
|
||||||
];
|
|
||||||
|
|
||||||
const processResponse = row => {
|
const processResponse = row => {
|
||||||
Object.keys(row).forEach(key => {
|
Object.keys(row).forEach(key => {
|
|
@ -0,0 +1,15 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const Util = require('../utils/Util');
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await Util.writeConfigToDb(Util.getEnvironmentDefaults());
|
||||||
|
console.log('Configuration overwriten, you can now start chibisafe');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
|
@ -23,7 +23,7 @@ const db = Knex({
|
||||||
some things like different data types for booleans need to be considered like in
|
some things like different data types for booleans need to be considered like in
|
||||||
the implementation below where sqlite returns 1 and 0 instead of true and false.
|
the implementation below where sqlite returns 1 and 0 instead of true and false.
|
||||||
*/
|
*/
|
||||||
const booleanFields = ['enabled', 'enableDownload', 'isAdmin', 'nsfw'];
|
const booleanFields = ['enabled', 'enableDownload', 'isAdmin', 'nsfw', 'generateZips', 'publicMode', 'userAccounts'];
|
||||||
|
|
||||||
const processResponse = row => {
|
const processResponse = row => {
|
||||||
Object.keys(row).forEach(key => {
|
Object.keys(row).forEach(key => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ const JWT = require('jsonwebtoken');
|
||||||
const db = require('./Database');
|
const db = require('./Database');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const log = require('../utils/Log');
|
const log = require('../utils/Log');
|
||||||
|
const Util = require('../utils/Util');
|
||||||
|
|
||||||
class Route {
|
class Route {
|
||||||
constructor(path, method, options) {
|
constructor(path, method, options) {
|
||||||
|
@ -30,7 +31,7 @@ class Route {
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
if (!token) return res.status(401).json({ message: 'No authorization header provided' });
|
if (!token) return res.status(401).json({ message: 'No authorization header provided' });
|
||||||
|
|
||||||
return JWT.verify(token, process.env.SECRET, async (error, decoded) => {
|
return JWT.verify(token, Util.config.secret, async (error, decoded) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return res.status(401).json({ message: 'Invalid token' });
|
return res.status(401).json({ message: 'Invalid token' });
|
||||||
|
|
|
@ -5,6 +5,11 @@ if (!process.env.SERVER_PORT) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.DOMAIN) {
|
||||||
|
console.log('You failed to provide a domain for your instance. Edit the .env file manually and fix it.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
const { loadNuxt, build } = require('nuxt');
|
const { loadNuxt, build } = require('nuxt');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
|
@ -19,11 +24,10 @@ const CronJob = require('cron').CronJob;
|
||||||
const log = require('../utils/Log');
|
const log = require('../utils/Log');
|
||||||
|
|
||||||
const Util = require('../utils/Util');
|
const Util = require('../utils/Util');
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const rateLimiter = new RateLimit({
|
const rateLimiter = new RateLimit({
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10),
|
windowMs: parseInt(Util.config.rateLimitWindow, 10),
|
||||||
max: parseInt(process.env.RATE_LIMIT_MAX, 10),
|
max: parseInt(Util.config.rateLimitMax, 10),
|
||||||
delayMs: 0
|
delayMs: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -72,8 +76,8 @@ class Server {
|
||||||
for (const File of routes) {
|
for (const File of routes) {
|
||||||
try {
|
try {
|
||||||
const route = new File();
|
const route = new File();
|
||||||
this.server[route.method](process.env.ROUTE_PREFIX + route.path, route.authorize.bind(route));
|
this.server[route.method](Util.config.routePrefix + route.path, route.authorize.bind(route));
|
||||||
log.info(`Found route ${route.method.toUpperCase()} ${process.env.ROUTE_PREFIX}${route.path}`);
|
log.info(`Found route ${route.method.toUpperCase()} ${Util.config.routePrefix}${route.path}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(`Failed loading route from file ${routeFile} with error: ${e.message}`);
|
log.error(`Failed loading route from file ${routeFile} with error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
@ -110,4 +114,10 @@ class Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new Server().start();
|
const start = async () => {
|
||||||
|
const conf = await Util.config;
|
||||||
|
new Server().start();
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const Joi = require('joi');
|
||||||
|
const { env } = process;
|
||||||
|
|
||||||
|
const StatsGenerator = require('../utils/StatsGenerator');
|
||||||
|
|
||||||
|
const Sections = Object.freeze({
|
||||||
|
SERVICE: 'Service',
|
||||||
|
FILE: 'File',
|
||||||
|
USERS: 'Users',
|
||||||
|
SOCIAL_AND_SHARING: 'Social and sharing',
|
||||||
|
INSTANCE: 'Instance',
|
||||||
|
STATISTICS: 'Statistics',
|
||||||
|
SERVER: 'Server',
|
||||||
|
OTHER: 'Other'
|
||||||
|
});
|
||||||
|
|
||||||
|
// use label to name them nicely
|
||||||
|
// use meta to set custom rendering (render as radio instead of dropdown for example) and custom order
|
||||||
|
// use description to add comments which will show up as a note somewhere next to the option
|
||||||
|
const schema = Joi.object({
|
||||||
|
// Service settings
|
||||||
|
serviceName: Joi.string().default('change-me')
|
||||||
|
// .meta({ section })
|
||||||
|
.meta({
|
||||||
|
section: Sections.SERVICE
|
||||||
|
})
|
||||||
|
.label('Service name')
|
||||||
|
.description('Name of the service'),
|
||||||
|
|
||||||
|
domain: Joi.string().default(`http://localhost:${env.SERVER_PORT}`)
|
||||||
|
.meta({
|
||||||
|
section: Sections.SERVICE
|
||||||
|
})
|
||||||
|
.label('Domain')
|
||||||
|
.description('Full domain this instance is gonna be running on'),
|
||||||
|
|
||||||
|
// File related settings
|
||||||
|
chunkSize: Joi.number().integer().greater(0)
|
||||||
|
.default(90)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Chunk size')
|
||||||
|
.description('Maximum size of a chunk (files bigger than this limit will be split into multiple chunks)'),
|
||||||
|
|
||||||
|
maxSize: Joi.number().integer().min(0) // setting it to 0 disabled the limit
|
||||||
|
.default(5000)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Maximum file size')
|
||||||
|
.description('Maximum allowed upload file size in MB (0 to disable)'),
|
||||||
|
|
||||||
|
generateZips: Joi.boolean().default(true)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Generate zips')
|
||||||
|
.description('Allows users to download entire albums in ZIP format'),
|
||||||
|
|
||||||
|
generatedFileNameLength: Joi.number().integer().min(6)
|
||||||
|
.default(12)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Generated file name length')
|
||||||
|
.description('How long should the automatically generated file name be'),
|
||||||
|
|
||||||
|
generatedAlbumLength: Joi.number().integer().min(6)
|
||||||
|
.default(6)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Generated album name length')
|
||||||
|
.description('How long should the automatically generated album identifier be'),
|
||||||
|
|
||||||
|
maxLinksPerAlbum: Joi.number().integer().greater(0)
|
||||||
|
.default(5)
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Maximum album links')
|
||||||
|
.description('Maximum allowed number of a distinct links for an album'),
|
||||||
|
|
||||||
|
uploadsFolder: Joi.string().default('uploads')
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Uploads folder')
|
||||||
|
.description('Name of the folder where the uploads will be stored'),
|
||||||
|
|
||||||
|
blockedExtensions: Joi.array()
|
||||||
|
.items(Joi.string().pattern(/^(\.\w+)+$/, { name: 'file extension' }))
|
||||||
|
.default(['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'])
|
||||||
|
.meta({
|
||||||
|
section: Sections.FILE
|
||||||
|
})
|
||||||
|
.label('Blocked extensions')
|
||||||
|
.description('List of extensions which will be rejected by the server'),
|
||||||
|
|
||||||
|
// User settings
|
||||||
|
publicMode: Joi.boolean().default(true)
|
||||||
|
.meta({
|
||||||
|
section: Sections.USERS
|
||||||
|
})
|
||||||
|
.label('Public mode')
|
||||||
|
.description('Allows people to upload files without an account'),
|
||||||
|
|
||||||
|
userAccount: Joi.boolean().default(true)
|
||||||
|
.meta({
|
||||||
|
section: Sections.USERS
|
||||||
|
})
|
||||||
|
.label('User creation')
|
||||||
|
.description('Allows people to create new accounts'),
|
||||||
|
|
||||||
|
// Social and sharing
|
||||||
|
metaThemeColor: Joi.string().pattern(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i).min(4)
|
||||||
|
.max(7)
|
||||||
|
.default('#20222b')
|
||||||
|
.meta({
|
||||||
|
section: Sections.SOCIAL_AND_SHARING
|
||||||
|
})
|
||||||
|
.label('Meta theme color')
|
||||||
|
.description('Color that user agents should use to customize the display of the page/embeds'),
|
||||||
|
|
||||||
|
metaDescription: Joi.string().default('Blazing fast file uploader and bunker written in node! 🚀')
|
||||||
|
.meta({
|
||||||
|
section: Sections.SOCIAL_AND_SHARING
|
||||||
|
})
|
||||||
|
.label('Meta description')
|
||||||
|
.description('Short and accurate summary of the content of the page'),
|
||||||
|
|
||||||
|
metaKeyword: Joi.string().default('chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free')
|
||||||
|
.meta({
|
||||||
|
section: Sections.SOCIAL_AND_SHARING
|
||||||
|
})
|
||||||
|
.label('Meta keywords')
|
||||||
|
.description('Words relevant to the page\'s content separated by commas'),
|
||||||
|
|
||||||
|
metaTwitterHandle: Joi.string().pattern(/^@\w{1,15}$/, { name: 'twitter handle' })
|
||||||
|
.meta({
|
||||||
|
section: Sections.SOCIAL_AND_SHARING
|
||||||
|
})
|
||||||
|
.label('Twitter handle')
|
||||||
|
.description('Your twitter handle'),
|
||||||
|
|
||||||
|
// Instance settings
|
||||||
|
backgroundImageURL: Joi.string().uri().default(p => `${p.domain}/assets/images/background.jpg`)
|
||||||
|
.meta({
|
||||||
|
section: Sections.INSTANCE
|
||||||
|
})
|
||||||
|
.label('Background image link')
|
||||||
|
.description('Background image that should be used instead of the default background'),
|
||||||
|
|
||||||
|
logoURL: Joi.string().uri().default(p => `${p.domain}/assets/images/logo.jpg`)
|
||||||
|
.meta({
|
||||||
|
section: Sections.INSTANCE
|
||||||
|
})
|
||||||
|
.label('Logo image link')
|
||||||
|
.description('Logo image that should be used instead of the default logo'),
|
||||||
|
|
||||||
|
// Statistics settings
|
||||||
|
// TODO: Pattern fails for patterns like 0 1,2-7 * * * * because of the mixing of lists and ranges
|
||||||
|
statisticsCron: Joi.string().pattern(/((((\d+,)+\d+|([\d\*]+(\/|-)\d+)|\d+|\*) ?){6})/, { name: 'cron' }).default('0 0 * * * *')
|
||||||
|
.meta({
|
||||||
|
section: Sections.STATISTICS
|
||||||
|
})
|
||||||
|
.label('Statistics schedule')
|
||||||
|
.description('Crontab like formated value which will be used to schedule generating and saving stats to the database'),
|
||||||
|
|
||||||
|
enabledStatistics: Joi.array().items(Joi.string().valid(...Object.keys(StatsGenerator.statGenerators)).optional())
|
||||||
|
.meta({ section: Sections.STATISTICS, displayType: 'checkbox' })
|
||||||
|
.label('Enabled statistics')
|
||||||
|
.description('Which statistics should be shown when opening the statistics page'),
|
||||||
|
|
||||||
|
savedStatistics: Joi.array().items(Joi.string().valid(...Object.keys(StatsGenerator.statGenerators)).optional())
|
||||||
|
.meta({ section: Sections.STATISTICS, displayType: 'checkbox' })
|
||||||
|
.label('Cached statistics')
|
||||||
|
.description('Which statistics should be saved to the database (refer to Statistics schedule for scheduling).')
|
||||||
|
.note('If a statistics is enabled but not set to be saved, it will be generated every time the statistics page is opened'),
|
||||||
|
|
||||||
|
// Server related settings
|
||||||
|
rateLimitWindow: Joi.number().integer().default(2)
|
||||||
|
.meta({ section: Sections.SERVER })
|
||||||
|
.label('API rate limit window')
|
||||||
|
.description('Timeframe for which requests are checked/remembered'),
|
||||||
|
|
||||||
|
rateLimitMax: Joi.number().integer().default(5)
|
||||||
|
.meta({ section: Sections.SERVER })
|
||||||
|
.label('API maximum limit')
|
||||||
|
.description('Max number of connections during windowMs milliseconds before sending a 429 response')
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.schema = schema;
|
||||||
|
module.exports.configSchema = schema.describe();
|
|
@ -10,11 +10,12 @@ class ThumbUtil {
|
||||||
static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
||||||
static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
||||||
|
|
||||||
static thumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs');
|
static thumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs');
|
||||||
static squareThumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'square');
|
static squareThumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'square');
|
||||||
static videoPreviewPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'preview');
|
static videoPreviewPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'preview');
|
||||||
|
|
||||||
static generateThumbnails(filename) {
|
static generateThumbnails(filename) {
|
||||||
|
if (!filename) return;
|
||||||
const ext = path.extname(filename).toLowerCase();
|
const ext = path.extname(filename).toLowerCase();
|
||||||
const output = `${filename.slice(0, -ext.length)}.webp`;
|
const output = `${filename.slice(0, -ext.length)}.webp`;
|
||||||
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
||||||
|
@ -27,7 +28,7 @@ class ThumbUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async generateThumbnailForImage(filename, output) {
|
static async generateThumbnailForImage(filename, output) {
|
||||||
const filePath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, filename);
|
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||||
|
|
||||||
const file = await jetpack.readAsync(filePath, 'buffer');
|
const file = await jetpack.readAsync(filePath, 'buffer');
|
||||||
await sharp(file)
|
await sharp(file)
|
||||||
|
@ -41,7 +42,7 @@ class ThumbUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async generateThumbnailForVideo(filename, output) {
|
static async generateThumbnailForVideo(filename, output) {
|
||||||
const filePath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, filename);
|
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||||
|
|
||||||
ffmpeg(filePath)
|
ffmpeg(filePath)
|
||||||
.thumbnail({
|
.thumbnail({
|
||||||
|
|
|
@ -12,37 +12,102 @@ const log = require('./Log');
|
||||||
const ThumbUtil = require('./ThumbUtil');
|
const ThumbUtil = require('./ThumbUtil');
|
||||||
const StatsGenerator = require('./StatsGenerator');
|
const StatsGenerator = require('./StatsGenerator');
|
||||||
|
|
||||||
const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(',');
|
|
||||||
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
||||||
|
|
||||||
let statsLastSavedTime = null;
|
|
||||||
|
|
||||||
class Util {
|
class Util {
|
||||||
static uploadPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER);
|
static uploadPath = path.join(__dirname, '../../../', 'uploads');
|
||||||
|
static statsLastSavedTime = null;
|
||||||
|
static _config = null;
|
||||||
|
|
||||||
|
static get config() {
|
||||||
|
if (this._config) return this._config;
|
||||||
|
return (async () => {
|
||||||
|
if (this._config === null) {
|
||||||
|
const conf = await db('settings').select('key', 'value');
|
||||||
|
this._config = conf.reduce((obj, item) => (
|
||||||
|
// eslint-disable-next-line no-sequences
|
||||||
|
obj[item.key] = typeof item.value === 'string' || item.value instanceof String ? JSON.parse(item.value) : item.value, obj
|
||||||
|
), {});
|
||||||
|
}
|
||||||
|
return this._config;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
static invalidateConfigCache() {
|
||||||
|
this._config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getEnvironmentDefaults() {
|
||||||
|
return {
|
||||||
|
domain: process.env.DOMAIN,
|
||||||
|
routePrefix: process.env.ROUTE_PREFIX || '/api',
|
||||||
|
rateLimitWindow: process.env.RATE_LIMIT_WINDOW || 2,
|
||||||
|
rateLimitMax: process.env.RATE_LIMIT_MAX || 5,
|
||||||
|
secret: process.env.SECRET || randomstring.generate(64),
|
||||||
|
serviceName: process.env.SERVICE_NAME || 'change-me',
|
||||||
|
chunkSize: process.env.CHUNK_SIZE || 90,
|
||||||
|
maxSize: process.env.MAX_SIZE || 5000,
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
generateZips: process.env.GENERATE_ZIPS == undefined ? true : false,
|
||||||
|
generatedFilenameLength: process.env.GENERATED_FILENAME_LENGTH || 12,
|
||||||
|
generatedAlbumLength: process.env.GENERATED_ALBUM_LENGTH || 6,
|
||||||
|
blockedExtensions: process.env.BLOCKED_EXTENSIONS ? process.env.BLOCKED_EXTENSIONS.split(',') : ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
publicMode: process.env.PUBLIC_MODE == undefined ? true : false,
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
userAccounts: process.env.USER_ACCOUNTS == undefined ? true : false,
|
||||||
|
metaThemeColor: process.env.META_THEME_COLOR || '#20222b',
|
||||||
|
metaDescription: process.env.META_DESCRIPTION || 'Blazing fast file uploader and bunker written in node! 🚀',
|
||||||
|
metaKeywords: process.env.META_KEYWORDS || 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
||||||
|
metaTwitterHandle: process.env.META_TWITTER_HANDLE || '@your-handle'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async wipeConfigDb() {
|
||||||
|
try {
|
||||||
|
await db.table('settings').del();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async writeConfigToDb(config, wipe = false) {
|
||||||
|
// TODO: Check that the config passes the joi schema validation
|
||||||
|
if (!config || !config.key) return;
|
||||||
|
try {
|
||||||
|
config.value = JSON.stringify(config.value);
|
||||||
|
await db.table('settings').insert(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.invalidateConfigCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static uuid() {
|
static uuid() {
|
||||||
return uuidv4();
|
return uuidv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
static isExtensionBlocked(extension) {
|
static isExtensionBlocked(extension) {
|
||||||
return blockedExtensions.includes(extension);
|
return this.config.blockedExtensions.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMimeFromType(fileTypeMimeObj) {
|
static getMimeFromType(fileTypeMimeObj) {
|
||||||
return fileTypeMimeObj ? fileTypeMimeObj.mime : undefined;
|
return fileTypeMimeObj ? fileTypeMimeObj.mime : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constructFilePublicLink(file) {
|
static constructFilePublicLink(req, file) {
|
||||||
/*
|
/*
|
||||||
TODO: This wont work without a reverse proxy serving both
|
TODO: This wont work without a reverse proxy serving both
|
||||||
the site and the API under the same domain. Pls fix.
|
the site and the API under the same domain. Pls fix.
|
||||||
*/
|
*/
|
||||||
file.url = `${process.env.DOMAIN}/${file.name}`;
|
const host = this.getHost(req);
|
||||||
|
file.url = `${host}/${file.name}`;
|
||||||
const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
|
const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`;
|
file.thumb = `${host}/thumbs/${thumb}`;
|
||||||
file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`;
|
file.thumbSquare = `${host}/thumbs/square/${thumb}`;
|
||||||
file.preview = preview && `${process.env.DOMAIN}/thumbs/preview/${preview}`;
|
file.preview = preview && `${host}/thumbs/preview/${preview}`;
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +115,7 @@ class Util {
|
||||||
static getUniqueFilename(extension) {
|
static getUniqueFilename(extension) {
|
||||||
const retry = (i = 0) => {
|
const retry = (i = 0) => {
|
||||||
const filename = randomstring.generate({
|
const filename = randomstring.generate({
|
||||||
length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10),
|
length: this.config.generatedFilenameLength,
|
||||||
capitalization: 'lowercase'
|
capitalization: 'lowercase'
|
||||||
}) + extension;
|
}) + extension;
|
||||||
|
|
||||||
|
@ -67,7 +132,7 @@ class Util {
|
||||||
static getUniqueAlbumIdentifier() {
|
static getUniqueAlbumIdentifier() {
|
||||||
const retry = async (i = 0) => {
|
const retry = async (i = 0) => {
|
||||||
const identifier = randomstring.generate({
|
const identifier = randomstring.generate({
|
||||||
length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10),
|
length: this.config.generatedAlbumLength,
|
||||||
capitalization: 'lowercase'
|
capitalization: 'lowercase'
|
||||||
});
|
});
|
||||||
const exists = await db
|
const exists = await db
|
||||||
|
@ -164,7 +229,7 @@ class Util {
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
if (!token) return false;
|
if (!token) return false;
|
||||||
|
|
||||||
return JWT.verify(token, process.env.SECRET, async (error, decoded) => {
|
return JWT.verify(token, this.config.secret, async (error, decoded) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return false;
|
return false;
|
||||||
|
@ -190,13 +255,7 @@ class Util {
|
||||||
zip.addLocalFile(path.join(Util.uploadPath, file));
|
zip.addLocalFile(path.join(Util.uploadPath, file));
|
||||||
}
|
}
|
||||||
zip.writeZip(
|
zip.writeZip(
|
||||||
path.join(
|
path.join(__dirname, '../../../', 'uploads', 'zips', `${album.userId}-${album.id}.zip`)
|
||||||
__dirname,
|
|
||||||
'../../../',
|
|
||||||
process.env.UPLOAD_FOLDER,
|
|
||||||
'zips',
|
|
||||||
`${album.userId}-${album.id}.zip`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
|
@ -205,8 +264,8 @@ class Util {
|
||||||
|
|
||||||
static generateThumbnails = ThumbUtil.generateThumbnails;
|
static generateThumbnails = ThumbUtil.generateThumbnails;
|
||||||
|
|
||||||
static async fileExists(res, exists, filename) {
|
static async fileExists(req, res, exists, filename) {
|
||||||
exists = Util.constructFilePublicLink(exists);
|
exists = Util.constructFilePublicLink(req, exists);
|
||||||
res.json({
|
res.json({
|
||||||
message: 'Successfully uploaded the file.',
|
message: 'Successfully uploaded the file.',
|
||||||
name: exists.name,
|
name: exists.name,
|
||||||
|
@ -214,7 +273,7 @@ class Util {
|
||||||
size: exists.size,
|
size: exists.size,
|
||||||
url: exists.url,
|
url: exists.url,
|
||||||
thumb: exists.thumb,
|
thumb: exists.thumb,
|
||||||
deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}`,
|
deleteUrl: `${this.getHost(req)}/api/file/${exists.id}`,
|
||||||
repeated: true
|
repeated: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -238,7 +297,7 @@ class Util {
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (dbFile) {
|
if (dbFile) {
|
||||||
await this.fileExists(res, dbFile, file.data.filename);
|
await this.fileExists(req, res, dbFile, file.data.filename);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +379,7 @@ class Util {
|
||||||
// skip generating and saving new stats.
|
// skip generating and saving new stats.
|
||||||
if (!force &&
|
if (!force &&
|
||||||
(!db.userParams.lastMutationTime ||
|
(!db.userParams.lastMutationTime ||
|
||||||
(statsLastSavedTime && statsLastSavedTime > db.userParams.lastMutationTime)
|
(Util.statsLastSavedTime && Util.statsLastSavedTime > db.userParams.lastMutationTime)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
@ -341,11 +400,15 @@ class Util {
|
||||||
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
||||||
}
|
}
|
||||||
|
|
||||||
statsLastSavedTime = now.getTime();
|
Util.statsLastSavedTime = now.getTime();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getHost(req) {
|
||||||
|
return `${req.protocol}://${req.headers.host}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Util;
|
module.exports = Util;
|
||||||
|
|
|
@ -6,7 +6,7 @@ const path = require('path');
|
||||||
const ThumbUtil = require('./ThumbUtil');
|
const ThumbUtil = require('./ThumbUtil');
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const files = fs.readdirSync(path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER));
|
const files = fs.readdirSync(path.join(__dirname, '../../../uploads'));
|
||||||
for (const fileName of files) {
|
for (const fileName of files) {
|
||||||
console.log(`Generating thumb for '${fileName}`);
|
console.log(`Generating thumb for '${fileName}`);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
|
110
src/setup.js
110
src/setup.js
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const randomstring = require('randomstring');
|
|
||||||
const jetpack = require('fs-jetpack');
|
const jetpack = require('fs-jetpack');
|
||||||
const qoa = require('qoa');
|
const qoa = require('qoa');
|
||||||
|
|
||||||
|
@ -16,53 +15,17 @@ async function start() {
|
||||||
const wizard = [
|
const wizard = [
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Port to run chibisafe in: (5000)',
|
query: 'Full domain this instance is gonna be running on (Ex: https://my-super-chibisafe.xyz):',
|
||||||
handle: 'SERVER_PORT'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
query: 'Full domain this instance is gonna be running on (Ex: https://chibisafe.moe):',
|
|
||||||
handle: 'DOMAIN'
|
handle: 'DOMAIN'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Name of the service? (Ex: chibisafe):',
|
query: 'Port to run chibisafe in? (default: 5000)',
|
||||||
handle: 'SERVICE_NAME'
|
handle: 'SERVER_PORT'
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
query: 'Maximum allowed upload file size in MB (Ex: 100):',
|
|
||||||
handle: 'MAX_SIZE'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
query: 'Allow users to download entire albums in ZIP format? (true)',
|
|
||||||
handle: 'GENERATE_ZIPS',
|
|
||||||
accept: 'y',
|
|
||||||
deny: 'n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
query: 'Allow people to upload files without an account? (true)',
|
|
||||||
handle: 'PUBLIC_MODE',
|
|
||||||
accept: 'y',
|
|
||||||
deny: 'n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
query: 'Allow people to create new accounts? (true)',
|
|
||||||
handle: 'USER_ACCOUNTS',
|
|
||||||
accept: 'y',
|
|
||||||
deny: 'n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
query: 'Name of the admin account? (admin)',
|
|
||||||
handle: 'ADMIN_ACCOUNT'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'interactive',
|
type: 'interactive',
|
||||||
query: 'Which predefined database do you want to use?',
|
query: 'Which database do you want to use? (select sqlite3 if not sure)',
|
||||||
handle: 'DB_CLIENT',
|
handle: 'DB_CLIENT',
|
||||||
symbol: '>',
|
symbol: '>',
|
||||||
menu: [
|
menu: [
|
||||||
|
@ -73,22 +36,22 @@ async function start() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Database host (Ignore if you selected sqlite3):',
|
query: 'Database host (Leave blank if you selected sqlite3):',
|
||||||
handle: 'DB_HOST'
|
handle: 'DB_HOST'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Database user (Ignore if you selected sqlite3):',
|
query: 'Database user (Leave blank if you selected sqlite3):',
|
||||||
handle: 'DB_USER'
|
handle: 'DB_USER'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Database password (Ignore if you selected sqlite3):',
|
query: 'Database password (Leave blank if you selected sqlite3):',
|
||||||
handle: 'DB_PASSWORD'
|
handle: 'DB_PASSWORD'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
query: 'Database name (Ignore if you selected sqlite3):',
|
query: 'Database name (Leave blank if you selected sqlite3):',
|
||||||
handle: 'DB_DATABASE'
|
handle: 'DB_DATABASE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -97,69 +60,30 @@ async function start() {
|
||||||
let envfile = '';
|
let envfile = '';
|
||||||
|
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
_1: '# Server settings',
|
DOMAIN: response.DOMAIN,
|
||||||
SERVER_PORT: response.SERVER_PORT || 5000,
|
SERVER_PORT: response.SERVER_PORT || 5000,
|
||||||
WEBSITE_PORT: 5001,
|
|
||||||
ROUTE_PREFIX: '/api',
|
|
||||||
RATE_LIMIT_WINDOW: 2,
|
|
||||||
RATE_LIMIT_MAX: 5,
|
|
||||||
SECRET: randomstring.generate(64),
|
|
||||||
|
|
||||||
_2: '# Service settings',
|
|
||||||
SERVICE_NAME: response.SERVICE_NAME || 'change-me',
|
|
||||||
DOMAIN: response.DOMAIN || `http://localhost:${response.SERVER_PORT}`,
|
|
||||||
|
|
||||||
_3: '# File related settings',
|
|
||||||
CHUNK_SIZE: 90,
|
|
||||||
MAX_SIZE: response.MAX_SIZE || 5000,
|
|
||||||
GENERATE_ZIPS: response.GENERATE_ZIPS == undefined ? true : false,
|
|
||||||
GENERATED_FILENAME_LENGTH: 12,
|
|
||||||
GENERATED_ALBUM_LENGTH: 6,
|
|
||||||
MAX_LINKS_PER_ALBUM: 5,
|
|
||||||
UPLOAD_FOLDER: 'uploads',
|
|
||||||
BLOCKED_EXTENSIONS: ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
|
||||||
|
|
||||||
_4: '# User settings',
|
|
||||||
PUBLIC_MODE: response.PUBLIC_MODE == undefined ? true : false,
|
|
||||||
USER_ACCOUNTS: response.USER_ACCOUNTS == undefined ? true : false,
|
|
||||||
ADMIN_ACCOUNT: response.ADMIN_ACCOUNT || 'admin',
|
|
||||||
ADMIN_PASSWORD: randomstring.generate(16),
|
|
||||||
|
|
||||||
_5: '# Database connection settings',
|
|
||||||
DB_CLIENT: response.DB_CLIENT,
|
DB_CLIENT: response.DB_CLIENT,
|
||||||
DB_HOST: response.DB_HOST || null,
|
DB_HOST: response.DB_HOST || null,
|
||||||
DB_USER: response.DB_USER || null,
|
DB_USER: response.DB_USER || null,
|
||||||
DB_PASSWORD: response.DB_PASSWORD || null,
|
DB_PASSWORD: response.DB_PASSWORD || null,
|
||||||
DB_DATABASE: response.DB_DATABASE || null,
|
DB_DATABASE: response.DB_DATABASE || null
|
||||||
|
|
||||||
_6: '# Social and sharing settings',
|
|
||||||
META_THEME_COLOR: '#20222b',
|
|
||||||
META_DESCRIPTION: 'Blazing fast file uploader and bunker written in node! 🚀',
|
|
||||||
META_KEYWORDS: 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
|
||||||
META_TWITTER_HANDLE: '@its_pitu'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = Object.keys(defaultSettings);
|
const keys = Object.keys(defaultSettings);
|
||||||
|
|
||||||
for (const item of keys) {
|
for (const item of keys) {
|
||||||
let prefix = `${item}=`;
|
envfile += `${item}=${defaultSettings[item]}\n`;
|
||||||
if (item.startsWith('_1')) {
|
|
||||||
prefix = '';
|
|
||||||
} else if (item.startsWith('_')) {
|
|
||||||
prefix = '\n';
|
|
||||||
}
|
|
||||||
envfile += `${prefix}${defaultSettings[item]}\n`;
|
|
||||||
}
|
}
|
||||||
jetpack.write('.env', envfile);
|
jetpack.write('.env', envfile);
|
||||||
jetpack.dir('database');
|
jetpack.dir('database');
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log('====================================================');
|
console.log('=====================================================');
|
||||||
console.log('== .env file generated successfully. ==');
|
console.log('== .env file generated successfully. ==');
|
||||||
console.log('====================================================');
|
console.log('=====================================================');
|
||||||
console.log(`== Your admin password is: ${defaultSettings.ADMIN_PASSWORD} ==`);
|
console.log(`== Both your initial user and password are 'admin' ==`);
|
||||||
console.log('== MAKE SURE TO CHANGE IT AFTER YOUR FIRST LOGIN! ==');
|
console.log('== MAKE SURE TO CHANGE IT AFTER YOUR FIRST LOGIN ==');
|
||||||
console.log('====================================================');
|
console.log('=====================================================');
|
||||||
console.log();
|
console.log();
|
||||||
setTimeout(() => {}, 1000);
|
setTimeout(() => {}, 1000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
href="https://github.com/pitu"
|
href="https://github.com/pitu"
|
||||||
class="no-block">Pitu</a>
|
class="no-block">Pitu</a>
|
||||||
</span><br>
|
</span><br>
|
||||||
<span>v{{ version }}</span>
|
<span>{{ version }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow bottom-up">
|
<div class="column is-narrow bottom-up">
|
||||||
<a href="https://github.com/weebdev/chibisafe">GitHub</a>
|
<a href="https://github.com/weebdev/chibisafe">GitHub</a>
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="settings">
|
||||||
|
<div v-for="[key, field] in Object.entries(settings)" :key="key">
|
||||||
|
<b-field
|
||||||
|
:label="field.flags.label"
|
||||||
|
:message="getErrorMessage(key) || field.flags.description"
|
||||||
|
:type="getValidationType(key)"
|
||||||
|
class="field"
|
||||||
|
horizontal>
|
||||||
|
<b-input
|
||||||
|
v-if="getDisplayType(field) === 'string'"
|
||||||
|
v-model="values[key]"
|
||||||
|
class="chibisafe-input"
|
||||||
|
expanded />
|
||||||
|
<b-input
|
||||||
|
v-else-if="getDisplayType(field) === 'number'"
|
||||||
|
v-model="values[key]"
|
||||||
|
type="number"
|
||||||
|
class="chibisafe-input"
|
||||||
|
:min="getMin(field)"
|
||||||
|
:max="getMax(field)"
|
||||||
|
expanded />
|
||||||
|
<b-switch
|
||||||
|
v-else-if="getDisplayType(field) === 'boolean'"
|
||||||
|
v-model="values[key]"
|
||||||
|
:rounded="false"
|
||||||
|
:true-value="true"
|
||||||
|
:false-value="false" />
|
||||||
|
<!-- TODO: If array and has allowed items, limit input to those items only -->
|
||||||
|
<b-taginput
|
||||||
|
v-else-if="getDisplayType(field) === 'array' || getDisplayType(field) === 'tagInput'"
|
||||||
|
v-model="values[key]"
|
||||||
|
ellipsis
|
||||||
|
icon="label"
|
||||||
|
:placeholder="field.flags.label"
|
||||||
|
class="taginp" />
|
||||||
|
<div v-else-if="getDisplayType(field) === 'checkbox'">
|
||||||
|
<b-checkbox v-for="item in getAllowedItems(field)" :key="item"
|
||||||
|
v-model="values[key]"
|
||||||
|
:native-value="item">
|
||||||
|
{{ item }}
|
||||||
|
</b-checkbox>
|
||||||
|
</div>
|
||||||
|
</b-field>
|
||||||
|
<!--
|
||||||
|
TODO: Add asterisk to required fields
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'JoiObject',
|
||||||
|
props: {
|
||||||
|
settings: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
'type': Object,
|
||||||
|
'default': () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
values: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
for (const [k, v] of Object.entries(this.settings)) {
|
||||||
|
this.$set(this.values, k, v.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getMin(field) {
|
||||||
|
if (field.type !== 'number') return;
|
||||||
|
|
||||||
|
for (const rule of field.rules) {
|
||||||
|
if (rule.name === 'greater') return rule.args.limit + 1;
|
||||||
|
if (rule.name === 'min') return rule.args.limit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMax(field) {
|
||||||
|
if (field.type !== 'number') return;
|
||||||
|
|
||||||
|
for (const rule of field.rules) {
|
||||||
|
if (rule.name === 'less') return rule.args.limit - 1;
|
||||||
|
if (rule.name === 'max') return rule.args.limit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDisplayType(field) {
|
||||||
|
if (!field.metas) return field.type;
|
||||||
|
|
||||||
|
const foundMeta = field.metas.find(e => e.displayType);
|
||||||
|
|
||||||
|
if (foundMeta) return foundMeta.displayType;
|
||||||
|
|
||||||
|
return field.type;
|
||||||
|
},
|
||||||
|
getAllowedItems(field) {
|
||||||
|
if (!field.items) return [];
|
||||||
|
|
||||||
|
return field.items.reduce((acc, item) => {
|
||||||
|
if (!item.allow) return acc;
|
||||||
|
|
||||||
|
return [...acc, ...item.allow];
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
getValidationType(fieldName) {
|
||||||
|
if (Array.isArray(this.errors[fieldName])) return 'is-danger';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getErrorMessage(fieldName) {
|
||||||
|
if (Array.isArray(this.errors[fieldName])) return this.errors[fieldName].join('\n');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getValues() {
|
||||||
|
return this.values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~/assets/styles/_colors.scss';
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
::v-deep .help.is-danger {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.taginp {
|
||||||
|
::v-deep {
|
||||||
|
.taginput-container {
|
||||||
|
border-color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input::placeholder {
|
||||||
|
color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taginput-container, .control, .input {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -124,7 +124,7 @@ export default {
|
||||||
parallelChunkUploads: false,
|
parallelChunkUploads: false,
|
||||||
chunkSize: this.config.chunkSize * 1000000,
|
chunkSize: this.config.chunkSize * 1000000,
|
||||||
chunksUploaded: this.dropzoneChunksUploaded,
|
chunksUploaded: this.dropzoneChunksUploaded,
|
||||||
maxFilesize: this.config.maxFileSize,
|
maxFilesize: this.config.maxUploadSize,
|
||||||
previewTemplate: this.$refs.template.innerHTML,
|
previewTemplate: this.$refs.template.innerHTML,
|
||||||
dictDefaultMessage: 'Drag & Drop your files or click to browse',
|
dictDefaultMessage: 'Drag & Drop your files or click to browse',
|
||||||
headers: { Accept: 'application/vnd.chibisafe.json' }
|
headers: { Accept: 'application/vnd.chibisafe.json' }
|
||||||
|
|
|
@ -11,112 +11,18 @@
|
||||||
</h2>
|
</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<b-field
|
<div v-for="[sectionName, fields] in Object.entries(sectionedSettings)" :key="sectionName" class="block">
|
||||||
label="Service name"
|
<h5 class="title is-5 has-text-grey-lighter">
|
||||||
message="Please enter the name which this service is gonna be identified as"
|
{{ sectionName }}
|
||||||
horizontal>
|
</h5>
|
||||||
<b-input
|
<JoiObject ref="jois" :settings="fields" :errors="validationErrors" />
|
||||||
v-model="settings.serviceName"
|
</div>
|
||||||
class="chibisafe-input"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Upload folder"
|
|
||||||
message="Where to store the files relative to the working directory"
|
|
||||||
horizontal>
|
|
||||||
<b-input
|
|
||||||
v-model="settings.uploadFolder"
|
|
||||||
class="chibisafe-input"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Links per album"
|
|
||||||
message="Maximum links allowed per album"
|
|
||||||
horizontal>
|
|
||||||
<b-input
|
|
||||||
v-model="settings.linksPerAlbum"
|
|
||||||
class="chibisafe-input"
|
|
||||||
type="number"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Max upload size"
|
|
||||||
message="Maximum allowed file size in MB"
|
|
||||||
horizontal>
|
|
||||||
<b-input
|
|
||||||
v-model="settings.maxUploadSize"
|
|
||||||
class="chibisafe-input"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Filename length"
|
|
||||||
message="How many characters long should the generated filenames be"
|
|
||||||
horizontal>
|
|
||||||
<b-input
|
|
||||||
v-model="settings.filenameLength"
|
|
||||||
class="chibisafe-input"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Album link length"
|
|
||||||
message="How many characters a link for an album should have"
|
|
||||||
horizontal>
|
|
||||||
<b-input
|
|
||||||
v-model="settings.albumLinkLength"
|
|
||||||
class="chibisafe-input"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Generate thumbnails"
|
|
||||||
message="Generate thumbnails when uploading a file if possible"
|
|
||||||
horizontal>
|
|
||||||
<b-switch
|
|
||||||
v-model="settings.generateThumbnails"
|
|
||||||
:true-value="true"
|
|
||||||
:false-value="false" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Generate zips"
|
|
||||||
message="Allow generating zips to download entire albums"
|
|
||||||
horizontal>
|
|
||||||
<b-switch
|
|
||||||
v-model="settings.generateZips"
|
|
||||||
:true-value="true"
|
|
||||||
:false-value="false" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Public mode"
|
|
||||||
message="Enable anonymous uploades"
|
|
||||||
horizontal>
|
|
||||||
<b-switch
|
|
||||||
v-model="settings.publicMode"
|
|
||||||
:true-value="true"
|
|
||||||
:false-value="false" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field
|
|
||||||
label="Enable creating account"
|
|
||||||
message="Enable creating new accounts in the platform"
|
|
||||||
horizontal>
|
|
||||||
<b-switch
|
|
||||||
v-model="settings.enableAccounts"
|
|
||||||
:true-value="true"
|
|
||||||
:false-value="false" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<div class="mb2 mt2 text-center">
|
<div class="mb2 mt2 text-center">
|
||||||
<button
|
<button
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
@click="promptRestartService">
|
@click="promptRestartService">
|
||||||
Save and restart service
|
Save settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,27 +34,69 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
||||||
|
import JoiObject from '~/components/settings/JoiObject.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Sidebar
|
Sidebar,
|
||||||
|
JoiObject
|
||||||
},
|
},
|
||||||
middleware: ['auth', 'admin'],
|
middleware: ['auth', 'admin'],
|
||||||
computed: mapState({
|
data() {
|
||||||
settings: state => state.admin.settings
|
return {
|
||||||
}),
|
validationErrors: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
settings: state => state.admin.settings,
|
||||||
|
settingsSchema: state => state.admin.settingsSchema
|
||||||
|
}),
|
||||||
|
sectionedSettings() {
|
||||||
|
return Object.entries(this.settingsSchema.keys).reduce((acc, [key, field]) => {
|
||||||
|
if (!field.metas) acc.Other = { ...acc.Other, [key]: field };
|
||||||
|
|
||||||
|
const sectionMeta = field.metas.find(e => e.section);
|
||||||
|
if (sectionMeta) {
|
||||||
|
acc[sectionMeta.section] = { ...acc[sectionMeta.section], [key]: field };
|
||||||
|
} else {
|
||||||
|
acc.Other = { ...acc.Other, [key]: field };
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
},
|
||||||
async asyncData({ app }) {
|
async asyncData({ app }) {
|
||||||
await app.store.dispatch('admin/fetchSettings');
|
await app.store.dispatch('admin/fetchSettings');
|
||||||
|
await app.store.dispatch('admin/getSettingsSchema');
|
||||||
|
await app.store.commit('admin/populateSchemaWithValues');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
promptRestartService() {
|
promptRestartService() {
|
||||||
this.$buefy.dialog.confirm({
|
this.$buefy.dialog.confirm({
|
||||||
message: 'Keep in mind that restarting only works if you have PM2 or something similar set up. Continue?',
|
message: 'Certain changes need for you to manually restart your chibisafe instance, please do so from the terminal. Continue?',
|
||||||
onConfirm: () => this.restartService()
|
onConfirm: () => this.saveSettings()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
restartService() {
|
async saveSettings() {
|
||||||
this.$handler.executeAction('admin/restartService');
|
// handle refs
|
||||||
|
let settings = {};
|
||||||
|
for (const joiComponent of this.$refs.jois) {
|
||||||
|
settings = { ...settings, ...joiComponent.getValues() };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('admin/saveSettings', settings);
|
||||||
|
this.$set(this, 'validationErrors', {});
|
||||||
|
|
||||||
|
await this.$store.dispatch('config/fetchSettings');
|
||||||
|
// this.$handler.executeAction('admin/restartService');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.response?.data?.errors) {
|
||||||
|
this.$set(this, 'validationErrors', e.response.data.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
|
|
|
@ -12,23 +12,38 @@ export const state = () => ({
|
||||||
},
|
},
|
||||||
file: {},
|
file: {},
|
||||||
settings: {},
|
settings: {},
|
||||||
statistics: {}
|
statistics: {},
|
||||||
|
settingsSchema: {
|
||||||
|
type: null,
|
||||||
|
keys: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
async fetchSettings({ commit }) {
|
async fetchSettings({ commit }) {
|
||||||
const response = await this.$axios.$get('service/config');
|
const response = await this.$axios.$get('service/config/all');
|
||||||
commit('setSettings', response);
|
commit('setSettings', response);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
async saveSettings({ commit }, settings) {
|
||||||
|
const response = await this.$axios.$post('service/config', { settings });
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
async fetchStatistics({ commit }, category) {
|
async fetchStatistics({ commit }, category) {
|
||||||
const url = category ? `service/statistics/${category}` : 'service/statistics';
|
const url = category ? `service/statistics/${category}` : 'service/statistics';
|
||||||
const response = await this.$axios.$get(url);
|
const response = await this.$axios.$get(url);
|
||||||
commit('setStatistics', { statistics: response.statistics, category: category });
|
commit('setStatistics', { statistics: response.statistics, category });
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
async getSettingsSchema({ commit }) {
|
||||||
|
// XXX: Maybe move to the config store?
|
||||||
|
const response = await this.$axios.$get('service/config/schema');
|
||||||
|
|
||||||
|
commit('setSettingsSchema', response);
|
||||||
|
},
|
||||||
async fetchUsers({ commit }) {
|
async fetchUsers({ commit }) {
|
||||||
const response = await this.$axios.$get('admin/users');
|
const response = await this.$axios.$get('admin/users');
|
||||||
commit('setUsers', response);
|
commit('setUsers', response);
|
||||||
|
@ -95,9 +110,6 @@ export const actions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setSettings(state, { config }) {
|
|
||||||
state.settings = config;
|
|
||||||
},
|
|
||||||
setStatistics(state, { statistics, category }) {
|
setStatistics(state, { statistics, category }) {
|
||||||
if (category) {
|
if (category) {
|
||||||
state.statistics[category] = statistics[category];
|
state.statistics[category] = statistics[category];
|
||||||
|
@ -105,6 +117,12 @@ export const mutations = {
|
||||||
state.statistics = statistics;
|
state.statistics = statistics;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setSettings(state, { config }) {
|
||||||
|
state.settings = config;
|
||||||
|
},
|
||||||
|
setSettingsSchema(state, { schema }) {
|
||||||
|
state.settingsSchema = schema;
|
||||||
|
},
|
||||||
setUsers(state, { users }) {
|
setUsers(state, { users }) {
|
||||||
state.users = users;
|
state.users = users;
|
||||||
},
|
},
|
||||||
|
@ -135,5 +153,12 @@ export const mutations = {
|
||||||
state.user.isAdmin = isAdmin;
|
state.user.isAdmin = isAdmin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
populateSchemaWithValues({ settings, settingsSchema }) {
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
if (settingsSchema.keys?.[key] !== undefined) {
|
||||||
|
settingsSchema.keys[key].value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,33 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
development: true,
|
development: process.env.development,
|
||||||
version: '4.0.0',
|
version: '',
|
||||||
URL: 'http://localhost:8080',
|
URL: process.env.development ? 'http://localhost:5000' : '/',
|
||||||
baseURL: 'http://localhost:8080/api',
|
baseURL: `${process.env.development ? 'http://localhost:5000' : ''}/api`,
|
||||||
serviceName: '',
|
serviceName: '',
|
||||||
maxFileSize: 100,
|
maxUploadSize: 0,
|
||||||
chunkSize: 90,
|
chunkSize: 0,
|
||||||
maxLinksPerAlbum: 5,
|
|
||||||
publicMode: false,
|
publicMode: false,
|
||||||
userAccounts: false
|
userAccounts: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mutations = {
|
export const actions = {
|
||||||
set(state, config) {
|
async fetchSettings({ commit }) {
|
||||||
Object.assign(state, config);
|
const response = await this.$axios.$get('service/config');
|
||||||
|
commit('setSettings', response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
setSettings(state, { config }) {
|
||||||
|
state.version = `v${config.version}`;
|
||||||
|
state.serviceName = config.serviceName;
|
||||||
|
state.maxUploadSize = config.maxUploadSize;
|
||||||
|
state.filenameLength = config.filenameLength;
|
||||||
|
state.albumLinkLength = config.albumLinkLength;
|
||||||
|
state.chunkSize = config.chunkSize;
|
||||||
|
state.publicMode = config.publicMode;
|
||||||
|
state.userAccounts = config.userAccounts;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import config from '../../../dist/config.json';
|
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
async nuxtServerInit({ commit, dispatch }) {
|
async nuxtServerInit({ commit, dispatch }) {
|
||||||
commit('config/set', config);
|
await dispatch('config/fetchSettings');
|
||||||
const cookies = this.$cookies.getAll();
|
const cookies = this.$cookies.getAll();
|
||||||
if (!cookies.token) return dispatch('auth/logout');
|
if (!cookies.token) return dispatch('auth/logout');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue