Merge pull request #278 from Zephyrrus/Zephyrrus-feature/database_based_settings

Zephyrrus feature/database based settings
This commit is contained in:
Kana 2021-06-19 02:03:57 +09:00 committed by GitHub
commit 065c5221a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 991 additions and 405 deletions

1
.gitignore vendored
View File

@ -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/

View File

@ -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

View File

@ -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"

View File

@ -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`.

View File

@ -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';
} }
} }
} }

194
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
} }

View File

@ -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');
};

View File

@ -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,

View File

@ -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');

View File

@ -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({

View File

@ -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({

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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' });

View File

@ -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.',

View File

@ -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;

View File

@ -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

View File

@ -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({

View File

@ -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({

View File

@ -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;

View File

@ -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'
} }
}); });
} }

View File

@ -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;

View File

@ -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;

View File

@ -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.',

View 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 => {

View File

@ -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();

View File

@ -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 => {

View File

@ -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' });

View File

@ -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();

View File

@ -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();

View File

@ -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({

View File

@ -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;

View File

@ -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

View File

@ -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);
} }

View File

@ -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>

View File

@ -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>

View File

@ -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' }

View File

@ -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() {

View File

@ -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;
}
}
} }
}; };

View File

@ -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;
} }
}; };

View File

@ -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');