diff --git a/.gitignore b/.gitignore index 08462849..fc0c073a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,20 @@ -.DS_Store .idea +.env +.DS_Store ._* +*.code-workspace +vendor +dist +backend/config.json +backend/internal/api/handler/assets +test/node_modules +*/node_modules +docs/.vuepress/dist +frontend/build +frontend/yarn-error.log +frontend/yarn.lock +frontend/.npmrc +test/cypress/fixtures/example.json .vscode -certbot-help.txt +docker-build +data \ No newline at end of file diff --git a/.version b/.version index 4db4b035..5efd7ac5 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.9.3 +3.0.0a diff --git a/DEV-README.md b/DEV-README.md new file mode 100644 index 00000000..05b8c63b --- /dev/null +++ b/DEV-README.md @@ -0,0 +1,27 @@ +# Nginx Proxy Manager 3 + +WIP + + +## Usage + +environment variables + + +## Building + +### Backend API Server + +```bash +go build -ldflags="-X main.commit=$(git log -n 1 --format=%h)" -o bin/server ./cmd/server/main.go +``` + + +## Development + +```bash +git clone nginxproxymanager +cd nginxproxymanager +./scripts/start-dev +curl http://127.0.0.1:3000/api/ +``` diff --git a/Jenkinsfile b/Jenkinsfile index 3161a254..cfe6c212 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,14 +8,17 @@ pipeline { ansiColor('xterm') } environment { - IMAGE = "nginx-proxy-manager" + IMAGE = 'nginx-proxy-manager' BUILD_VERSION = getVersion() - MAJOR_VERSION = "2" + BUILD_COMMIT = getCommit() + MAJOR_VERSION = '3' BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_FILE = 'docker/docker-compose.ci.yml' COMPOSE_INTERACTIVE_NO_CLI = 1 BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" + DOCS_BUCKET = 'jc21-npm-site-next' // TODO: change to prod when official + DOCS_CDN = 'E2Z0128EHS0Q23' // TODO: same } stages { stage('Environment') { @@ -45,10 +48,9 @@ pipeline { } stage('Versions') { steps { + // Is this frontend version stuff still applicable? sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json' sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"' - sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json' - sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"' sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' } } @@ -56,78 +58,57 @@ pipeline { } stage('Frontend') { steps { - sh './scripts/frontend-build' + sh './scripts/ci/frontend-build' + } + post { + always { + junit 'frontend/eslint.xml' + junit 'frontend/junit.xml' + } } } stage('Backend') { steps { - echo 'Checking Syntax ...' - // See: https://github.com/yarnpkg/yarn/issues/3254 - sh '''docker run --rm \\ - -v "$(pwd)/backend:/app" \\ - -v "$(pwd)/global:/app/global" \\ - -w /app \\ - node:latest \\ - sh -c "yarn install && yarn eslint . && rm -rf node_modules" - ''' - - echo 'Docker Build ...' - sh '''docker build --pull --no-cache --squash --compress \\ - -t "${IMAGE}:ci-${BUILD_NUMBER}" \\ - -f docker/Dockerfile \\ - --build-arg TARGETPLATFORM=linux/amd64 \\ - --build-arg BUILDPLATFORM=linux/amd64 \\ - --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ - --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ - --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ - . - ''' - } - } - stage('Integration Tests Sqlite') { - steps { - // Bring up a stack - sh 'docker-compose up -d fullstack-sqlite' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-sqlite' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/' - } - post { - always { - // Dumps to analyze later - sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' - // Cypress videos and screenshot artifacts - dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' - } - junit 'test/results/junit/*' + withCredentials([usernamePassword(credentialsId: 'oss-index-token', passwordVariable: 'NANCY_TOKEN', usernameVariable: 'NANCY_USER')]) { + sh '''docker build --pull --no-cache --squash --compress \\ + -t ${IMAGE}:ci-${BUILD_NUMBER} \\ + -f docker/Dockerfile \\ + --build-arg TARGETPLATFORM=linux/amd64 \\ + --build-arg BUILDPLATFORM=linux/amd64 \\ + --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ + --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ + --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ + --build-arg SENTRY_DSN="${SENTRY_DSN:-}" \\ + --build-arg GOPROXY="${GOPROXY:-}" \\ + --build-arg GOPRIVATE="${GOPRIVATE:-}" \\ + --build-arg NANCY_USER="${NANCY_USER}" \\ + --build-arg NANCY_TOKEN="${NANCY_TOKEN}" \\ + . + ''' } } } - stage('Integration Tests Mysql') { + stage('Test') { + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } steps { // Bring up a stack - sh 'docker-compose up -d fullstack-mysql' - sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120' - + sh 'docker-compose up -d fullstack' + sh './scripts/wait-healthy $(docker-compose ps -q fullstack) 120' // Run tests sh 'rm -rf test/results' - sh 'docker-compose up cypress-mysql' + sh 'docker-compose up cypress' // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/' + sh 'docker cp -L "$(docker-compose ps -q cypress):/test/results" test/' } post { always { // Dumps to analyze later sh 'mkdir -p debug' - sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz' - sh 'docker-compose logs db | gzip > debug/docker_db.log.gz' + sh 'docker-compose logs fullstack | gzip > debug/docker_fullstack.log.gz' // Cypress videos and screenshot artifacts dir(path: 'test/results') { archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' @@ -148,6 +129,11 @@ pipeline { sh 'yarn build' } + // API Docs: + sh 'docker-compose exec -T fullstack curl -s --output /temp-docs/api-schema.json "http://fullstack:81/api/schema"' + sh 'mkdir -p "docs/.vuepress/dist/api"' + sh 'mv docs/api-schema.json docs/.vuepress/dist/api/' + dir(path: 'docs/.vuepress/dist') { sh 'tar -czf ../../docs.tgz *' } @@ -155,21 +141,29 @@ pipeline { archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) } } + /* stage('MultiArch Build') { when { - not { - equals expected: 'UNSTABLE', actual: currentBuild.result + allOf { + branch 'master' + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } } } steps { - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - // Docker Login - sh "docker login -u '${duser}' -p '${dpass}'" - // Buildx with push from cache - sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" + withCredentials([string(credentialsId: 'npm-sentry-dsn', variable: 'SENTRY_DSN')]) { + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh "docker login -u '${duser}' -p '${dpass}'" + // Buildx to local files + // sh './scripts/buildx -o type=local,dest=docker-build' + // Buildx to with push + sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" + } } } } + */ stage('Docs Deploy') { when { allOf { @@ -183,7 +177,7 @@ pipeline { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh """docker run --rm \\ --name \${COMPOSE_PROJECT_NAME}-docs-upload \\ - -e S3_BUCKET=jc21-npm-site \\ + -e S3_BUCKET=$DOCS_BUCKET \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ -v \$(pwd):/app \\ @@ -197,7 +191,7 @@ pipeline { -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ jc21/ci-tools \\ - aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*' + aws cloudfront create-invalidation --distribution-id $DOCS_CDN --paths '/*' """ } } @@ -213,7 +207,40 @@ pipeline { } steps { script { - def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") + def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:git-3-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") + } + } + } + stage('Artifacts') { + when { + allOf { + branch 'master' + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + } + steps { + sh 'mkdir -p artifacts' + // Docs + dir(path: 'docs/.vuepress/dist') { + sh 'zip -qr ../../../artifacts/docs.zip *' + } + // Multiarch builds + /* + dir(path: 'docker-build/linux_amd64/app') { + sh 'zip -qr ../../../artifacts/linux_amd64.zip *' + } + dir(path: 'docker-build/linux_arm64/app') { + sh 'zip -qr ../../../artifacts/linux_arm64.zip *' + } + dir(path: 'docker-build/linux_arm_v7/app') { + sh 'zip -qr ../../../artifacts/linux_arm_v7.zip *' + } + **/ + // Archive them + dir(path: 'artifacts') { + archiveArtifacts artifacts: '**/*' } } } @@ -221,8 +248,9 @@ pipeline { post { always { sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' + sh './scripts/build-cleanup' sh 'echo Reverting ownership' - sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' + sh 'docker run --rm -v $(pwd):/data node:latest chown -R "$(id -u):$(id -g)" /data' } success { juxtapose event: 'success' diff --git a/README.md b/README.md index d50e5e7d..f00e86b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@



- + @@ -14,16 +14,12 @@ Gitter - - Reddit - -

This project comes as a pre-built docker image that enables you to easily forward to your websites running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. -- [Quick Setup](#quick-setup) +- [Quick Setup](https://nginxproxymanager.com#quick-setup) - [Full Setup](https://nginxproxymanager.com/setup/) - [Screenshots](https://nginxproxymanager.com/screenshots/) @@ -56,67 +52,6 @@ I won't go in to too much detail here but here are the basics for someone new to 3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) 4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services -## Quick Setup - -1. Install Docker and Docker-Compose - -- [Docker Install documentation](https://docs.docker.com/install/) -- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) - -2. Create a docker-compose.yml file similar to this: - -```yml -version: '3' -services: - app: - image: 'jc21/nginx-proxy-manager:latest' - restart: unless-stopped - ports: - - '80:80' - - '81:81' - - '443:443' - environment: - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" - volumes: - - ./data:/data - - ./letsencrypt:/etc/letsencrypt - db: - image: 'jc21/mariadb-aria:latest' - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: 'npm' - MYSQL_DATABASE: 'npm' - MYSQL_USER: 'npm' - MYSQL_PASSWORD: 'npm' - volumes: - - ./data/mysql:/var/lib/mysql -``` - -3. Bring up your stack - -```bash -docker-compose up -d -``` - -4. Log in to the Admin UI - -When your docker container is running, connect to it on port `81` for the admin interface. -Sometimes this can take a little bit because of the entropy of keys. - -[http://127.0.0.1:81](http://127.0.0.1:81) - -Default Admin User: -``` -Email: admin@example.com -Password: changeme -``` - -Immediately after logging in with this default user you will be asked to modify your details and change your password. - ## Contributors @@ -128,43 +63,43 @@ Special thanks to the following contributors: - +
Sebastian Valle
- +
Kyle Klaus
- +
ƬHE ЯAW
- +
Spencer
- +
Xantios Krugor
- +
David Panesso
- +
IronTooch
@@ -172,43 +107,43 @@ Special thanks to the following contributors: - +
Damiano
- +
Russ
- +
Marcelo Castagna
- +
Steven Harris
- +
Jocelyn Le Sage
- +
Carl Mercier
- +
Paul Mansfield
@@ -216,43 +151,43 @@ Special thanks to the following contributors: - +
OhHeyAlan
- +
Carl Sutton
- +
Gergő Törcsvári
- +
vrenjith
- +
David Rivera
- +
Jaap-Jan de Wit
- +
James Morgan
@@ -260,160 +195,22 @@ Special thanks to the following contributors: - +
chaptergy
- +
Philip Mooney
- +
WaterCalm
- - - -
lebrou34 -
- - - - -
Mário Franco -
- - - - -
Kyle Harding -
- - - - -
Alex Graber -
- - - - - - -
MooBaloo -
- - - - -
Shuro -
- - - - -
Loris Bergeron -
- - - - -
hepelayo -
- - - - -
Jonas Leder -
- - - - -
Bastian Stegmann -
- - - - -
Stealthii -
- - - - - - -
THEGamingninja -
- - - - -
Italo Borssatto -
- - - - -
Gurjinder Singh -
- - - - -
David Dosoudil -
- - - - -
ijaron -
- - - - -
Niels Bouma -
- - - - -
Orko Garai -
- - - - - - -
Filippo Baruffaldi -
- - - - -
Bikramjeet Singh -
- - - - -
Razvan Stoica -
- - - - -
RBXII3 -
- diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 00000000..8b96428f --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json deleted file mode 100644 index 6d6172a4..00000000 --- a/backend/.eslintrc.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "extends": [ - "eslint:recommended" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "align-assignments" - ], - "rules": { - "arrow-parens": [ - "error", - "always" - ], - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "key-spacing": [ - "error", - { - "align": "value" - } - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "func-call-spacing": [ - "error", - "never" - ], - "keyword-spacing": [ - "error", - { - "before": true - } - ], - "no-irregular-whitespace": "error", - "no-unused-expressions": 0, - "align-assignments/align-assignments": [ - 2, - { - "requiresOnly": false - } - ] - } -} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 149080b9..00000000 --- a/backend/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -config/development.json -data/* -yarn-error.log -tmp -certbot.log -node_modules -core.* - diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 00000000..f61dc69d --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,92 @@ +linters: + enable: + # Prevents against memory leaks in production caused by not closing file handle + - bodyclose + # Detects unused declarations in a go package + - deadcode + # Detects cloned code. DRY is good programming practice. Can cause issues with testing code where + # simplicity is preferred over duplication. Disabled for test code. + #- dupl + # Detects unchecked errors in go programs. These unchecked errors can be critical bugs in some cases. + - errcheck + # Simplifies go code. + - gosimple + # Reports suspicious constructs, maintained by goteam. e.g. Printf unused params not caught + # at compile time. + - govet + # Detect security issues with gocode. Use of secrets in code or obsolete security algorithms. + # It's imaged heuristic methods are used in finding problems. If issues with rules are found + # particular rules can be disabled as required. + # Could possibility cause issues with testing. Disabled for test code. + - gosec + # Detect repeated strings that could be replaced by a constant + - goconst + # Misc linters missing from other projects. Grouped into 3 categories diagnostics, style + # and performance + - gocritic + # Limits code cyclomatic complexity + - gocyclo + # Detects if code needs to be gofmt'd + - gofmt + # Detects unused go package imports + - goimports + # Detcts style mistakes not correctness. Golint is meant to carry out the + # stylistic conventions put forth in Effective Go and CodeReviewComments. + # golint has false positives and false negatives and can be tweaked. + - golint + # Detects ineffectual assignments in code + - ineffassign + # Detect commonly misspelled english words in comments + - misspell + # Detect naked returns on non-trivial functions, and conform with Go CodeReviewComments + - nakedret + # Detect slice allocations that can be preallocated + - prealloc + # Misc collection of static analysis tools + - staticcheck + # Detects unused struct fields + - structcheck + # Parses and typechecks the code like the go compiler + - typecheck + # Detects unused constants, variables, functions and types + - unused + # Detects unused global variables and constants + - varcheck + # Remove unnecessary type conversions + - unconvert + # Remove unnecessary(unused) function parameters + - unparam +linters-settings: + goconst: + # minimal length of string constant + # default: 3 + min-len: 2 + # minimum number of occurrences of string constant + # default: 3 + min-occurences: 2 + misspell: + locale: UK + ignore-words: + - color +issues: + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + # We have chosen an arbitrary value that works based on practical usage. + max-same: 20 + # See cmdline flag documentation for more info about default excludes --exclude-use-default + # Nothing is excluded by default + exclude-use-default: false + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. # TODO: Add examples why this is good + + - path: _test\.go + linters: + # Tests should be simple? Add example why this is good? + - gocyclo + # Error checking adds verbosity and complexity for minimal value + - errcheck + # Table test encourage duplication in defining the table tests. + - dupl + # Hard coded example tokens, SQL injection and other bad practices may + # want to be tested + - gosec diff --git a/backend/.nancy-ignore b/backend/.nancy-ignore new file mode 100644 index 00000000..5736e87a --- /dev/null +++ b/backend/.nancy-ignore @@ -0,0 +1,22 @@ +# If you need to ignore any of nancy's warnings add them +# here with a reference to the package/version that +# triggers them and rational for ignoring it. + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# etcd before versions 3.3.23 and 3.4.10 does not perform any password length validation +CVE-2020-15115 + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# In ectd before versions 3.4.10 and 3.3.23, gateway TLS authentication is only applied to endpoints detected in DNS SRV records +CVE-2020-15136 + +# pkg:golang/github.com/coreos/etcd@3.3.10 +# In etcd before versions 3.3.23 and 3.4.10, the etcd gateway is a simple TCP proxy to allow for basic service discovery and access +CVE-2020-15114 + +# pkg:golang/github.com/gorilla/websocket@1.4.0 +# Integer Overflow or Wraparound +CWE-190 + +# jwt-go before 4.0.0-preview1 allows attackers to bypass intended access restrict... +CVE-2020-26160 diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json deleted file mode 100644 index 4e540ab3..00000000 --- a/backend/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "editor.insertSpaces": false, - "editor.formatOnSave": true, - "files.trimTrailingWhitespace": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } -} \ No newline at end of file diff --git a/backend/Taskfile.yml b/backend/Taskfile.yml new file mode 100644 index 00000000..dd49f17c --- /dev/null +++ b/backend/Taskfile.yml @@ -0,0 +1,56 @@ +version: '2' + +tasks: + default: + cmds: + - task: run + + run: + desc: Build and run + sources: + - internal/**/*.go + - cmd/**/*.go + cmds: + - task: build + - cmd: echo -e "==> Running..." + silent: true + - cmd: ../dist/bin/server + ignore_error: true + silent: true + env: + LOG_LEVEL: debug + + build: + desc: Build the server + cmds: + - cmd: echo -e "==> Building..." + silent: true + - cmd: rm -f dist/bin/* + silent: true + - cmd: go build -ldflags="-X main.commit={{.GIT_COMMIT}} -X main.version={{.VERSION}}" -o ../dist/bin/server ./cmd/server/main.go + silent: true + - task: lint + vars: + GIT_COMMIT: + sh: git log -n 1 --format=%h + VERSION: + sh: cat ../.version + env: + GO111MODULE: on + CGO_ENABLED: 1 + + lint: + desc: Linting + cmds: + - cmd: echo -e "==> Linting..." + silent: true + - cmd: bash scripts/lint.sh + silent: true + + test: + desc: Testing + cmds: + - cmd: echo -e "==> Testing..." + silent: true + - cmd: bash scripts/test.sh + silent: true diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index 33ffacc5..00000000 --- a/backend/app.js +++ /dev/null @@ -1,90 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const fileUpload = require('express-fileupload'); -const compression = require('compression'); -const log = require('./logger').express; - -/** - * App - */ -const app = express(); -app.use(fileUpload()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: true})); - -// Gzip -app.use(compression()); - -/** - * General Logging, BEFORE routes - */ - -app.disable('x-powered-by'); -app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); -app.enable('strict routing'); - -// pretty print JSON when not live -if (process.env.NODE_ENV !== 'production') { - app.set('json spaces', 2); -} - -// CORS for everything -app.use(require('./lib/express/cors')); - -// General security/cache related headers + server header -app.use(function (req, res, next) { - let x_frame_options = 'DENY'; - - if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) { - x_frame_options = process.env.X_FRAME_OPTIONS; - } - - res.set({ - 'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload', - 'X-XSS-Protection': '1; mode=block', - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': x_frame_options, - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - Pragma: 'no-cache', - Expires: 0 - }); - next(); -}); - -app.use(require('./lib/express/jwt')()); -app.use('/', require('./routes/api/main')); - -// production error handler -// no stacktraces leaked to user -// eslint-disable-next-line -app.use(function (err, req, res, next) { - - let payload = { - error: { - code: err.status, - message: err.public ? err.message : 'Internal Error' - } - }; - - if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) { - payload.debug = { - stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null, - previous: err.previous - }; - } - - // Not every error is worth logging - but this is good for now until it gets annoying. - if (typeof err.stack !== 'undefined' && err.stack) { - if (process.env.NODE_ENV === 'development') { - log.debug(err.stack); - } else if (typeof err.public == 'undefined' || !err.public) { - log.warn(err.message); - } - } - - res - .status(err.status || 500) - .send(payload); -}); - -module.exports = app; diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 00000000..0875c68f --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "npm/internal/api" + "npm/internal/config" + "npm/internal/database" + "npm/internal/entity/setting" + "npm/internal/logger" + "npm/internal/state" + "npm/internal/worker" +) + +var commit string +var version string +var sentryDSN string + +func main() { + config.Init(&version, &commit, &sentryDSN) + appstate := state.NewState() + + setting.ApplySettings() + database.CheckSetup() + + go worker.StartCertificateWorker(appstate) + + api.StartServer() + irqchan := make(chan os.Signal, 1) + signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM) + + for irq := range irqchan { + if irq == syscall.SIGINT || irq == syscall.SIGTERM { + logger.Info("Got ", irq, " shutting server down ...") + // Close db + err := database.GetInstance().Close() + if err != nil { + logger.Error("DatabaseCloseError", err) + } + break + } + } +} diff --git a/backend/config/README.md b/backend/config/README.md deleted file mode 100644 index 26268a11..00000000 --- a/backend/config/README.md +++ /dev/null @@ -1,2 +0,0 @@ -These files are use in development and are not deployed as part of the final product. - \ No newline at end of file diff --git a/backend/config/default.json b/backend/config/default.json deleted file mode 100644 index 64ab577c..00000000 --- a/backend/config/default.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "database": { - "engine": "mysql", - "host": "db", - "name": "npm", - "user": "npm", - "password": "npm", - "port": 3306 - } -} diff --git a/backend/config/sqlite-test-db.json b/backend/config/sqlite-test-db.json deleted file mode 100644 index ad548865..00000000 --- a/backend/config/sqlite-test-db.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "database": { - "engine": "knex-native", - "knex": { - "client": "sqlite3", - "connection": { - "filename": "/app/config/mydb.sqlite" - }, - "pool": { - "min": 0, - "max": 1, - "createTimeoutMillis": 3000, - "acquireTimeoutMillis": 30000, - "idleTimeoutMillis": 30000, - "reapIntervalMillis": 1000, - "createRetryIntervalMillis": 100, - "propagateCreateError": false - }, - "migrations": { - "tableName": "migrations", - "stub": "src/backend/lib/migrate_template.js", - "directory": "src/backend/migrations" - } - } - } -} diff --git a/backend/db.js b/backend/db.js deleted file mode 100644 index ce5338f0..00000000 --- a/backend/db.js +++ /dev/null @@ -1,33 +0,0 @@ -const config = require('config'); - -if (!config.has('database')) { - throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md'); -} - -function generateDbConfig() { - if (config.database.engine === 'knex-native') { - return config.database.knex; - } else - return { - client: config.database.engine, - connection: { - host: config.database.host, - user: config.database.user, - password: config.database.password, - database: config.database.name, - port: config.database.port - }, - migrations: { - tableName: 'migrations' - } - }; -} - - -let data = generateDbConfig(); - -if (typeof config.database.version !== 'undefined') { - data.version = config.database.version; -} - -module.exports = require('knex')(data); diff --git a/backend/doc/api.swagger.json b/backend/doc/api.swagger.json index 06c02564..26bb1577 100644 --- a/backend/doc/api.swagger.json +++ b/backend/doc/api.swagger.json @@ -2,1253 +2,233 @@ "openapi": "3.0.0", "info": { "title": "Nginx Proxy Manager API", - "version": "2.x.x" + "version": "{{VERSION}}" }, - "servers": [ - { - "url": "http://127.0.0.1:81/api" - } - ], "paths": { "/": { "get": { - "operationId": "health", - "summary": "Returns the API health status", - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "status": "OK", - "version": { - "major": 2, - "minor": 1, - "revision": 0 - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/HealthObject" - } - } - } - } - } + "$ref": "file://./paths/get.json" + } + }, + "/certificates": { + "get": { + "$ref": "file://./paths/certificates/get.json" + }, + "post": { + "$ref": "file://./paths/certificates/post.json" + } + }, + "/certificates/{certificateID}": { + "get": { + "$ref": "file://./paths/certificates/certificateID/get.json" + }, + "put": { + "$ref": "file://./paths/certificates/certificateID/put.json" + }, + "delete": { + "$ref": "file://./paths/certificates/certificateID/delete.json" + } + }, + "/certificates-authorities": { + "get": { + "$ref": "file://./paths/certificates-authorities/get.json" + }, + "post": { + "$ref": "file://./paths/certificates-authorities/post.json" + } + }, + "/certificates-authorities/{caID}": { + "get": { + "$ref": "file://./paths/certificates-authorities/caID/get.json" + }, + "put": { + "$ref": "file://./paths/certificates-authorities/caID/put.json" + }, + "delete": { + "$ref": "file://./paths/certificates-authorities/caID/delete.json" + } + }, + "/config": { + "get": { + "$ref": "file://./paths/config/get.json" + } + }, + "/dns-providers": { + "get": { + "$ref": "file://./paths/dns-providers/get.json" + }, + "post": { + "$ref": "file://./paths/dns-providers/post.json" + } + }, + "/dns-providers/{providerID}": { + "get": { + "$ref": "file://./paths/dns-providers/providerID/get.json" + }, + "put": { + "$ref": "file://./paths/dns-providers/providerID/put.json" + }, + "delete": { + "$ref": "file://./paths/dns-providers/providerID/delete.json" + } + }, + "/hosts": { + "get": { + "$ref": "file://./paths/hosts/get.json" + }, + "post": { + "$ref": "file://./paths/hosts/post.json" + } + }, + "/hosts/{hostID}": { + "get": { + "$ref": "file://./paths/hosts/hostID/get.json" + }, + "put": { + "$ref": "file://./paths/hosts/hostID/put.json" + }, + "delete": { + "$ref": "file://./paths/hosts/hostID/delete.json" } }, "/schema": { "get": { - "operationId": "schema", - "responses": { - "200": { - "description": "200 response" - } - }, - "summary": "Returns this swagger API schema" - } - }, - "/tokens": { - "get": { - "operationId": "refreshToken", - "summary": "Refresh your access token", - "tags": [ - "Tokens" - ], - "security": [ - { - "BearerAuth": [ - "tokens" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - } - } - } - }, - "post": { - "operationId": "requestToken", - "parameters": [ - { - "description": "Credentials Payload", - "in": "body", - "name": "credentials", - "required": true, - "schema": { - "additionalProperties": false, - "properties": { - "identity": { - "minLength": 1, - "type": "string" - }, - "scope": { - "minLength": 1, - "type": "string", - "enum": [ - "user" - ] - }, - "secret": { - "minLength": 1, - "type": "string" - } - }, - "required": [ - "identity", - "secret" - ], - "type": "object" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "result": { - "expires": 1566540510, - "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/TokenObject" - } - } - }, - "description": "200 response" - } - }, - "summary": "Request a new access token from credentials", - "tags": [ - "Tokens" - ] + "$ref": "file://./paths/schema/get.json" } }, "/settings": { "get": { - "operationId": "getSettings", - "summary": "Get all settings", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/SettingsList" - } - } - } - } - } + "$ref": "file://./paths/settings/get.json" + }, + "post": { + "$ref": "file://./paths/settings/post.json" } }, - "/settings/{settingID}": { + "/settings/{name}": { "get": { - "operationId": "getSetting", - "summary": "Get a setting", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } + "$ref": "file://./paths/settings/name/get.json" }, "put": { - "operationId": "updateSetting", - "summary": "Update a setting", - "tags": [ - "Settings" - ], - "security": [ - { - "BearerAuth": [ - "settings" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "settingID", - "schema": { - "type": "string", - "minLength": 1 - }, - "required": true, - "description": "Setting ID", - "example": "default-site" - }, - { - "in": "body", - "name": "setting", - "description": "Setting Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": "default-site", - "name": "Default Site", - "description": "What to show when Nginx is hit with an unknown Host", - "value": "congratulations", - "meta": {} - } - } - }, - "schema": { - "$ref": "#/components/schemas/SettingObject" - } - } - } - } - } + "$ref": "file://./paths/settings/name/put.json" + } + }, + "/streams": { + "get": { + "$ref": "file://./paths/streams/get.json" + }, + "post": { + "$ref": "file://./paths/streams/post.json" + } + }, + "/streams/{streamID}": { + "get": { + "$ref": "file://./paths/streams/streamID/get.json" + }, + "put": { + "$ref": "file://./paths/streams/streamID/put.json" + }, + "delete": { + "$ref": "file://./paths/streams/streamID/delete.json" + } + }, + "/tokens": { + "get": { + "$ref": "file://./paths/tokens/get.json" + }, + "post": { + "$ref": "file://./paths/tokens/post.json" } }, "/users": { "get": { - "operationId": "getUsers", - "summary": "Get all users", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "query", - "name": "expand", - "description": "Expansions", - "schema": { - "type": "string", - "enum": [ - "permissions" - ] - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - ] - }, - "withPermissions": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - ] - } - }, - "schema": { - "$ref": "#/components/schemas/UsersList" - } - } - } - } - } + "$ref": "file://./paths/users/get.json" }, "post": { - "operationId": "createUser", - "summary": "Create a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "201": { - "description": "201 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } + "$ref": "file://./paths/users/post.json" } }, "/users/{userID}": { "get": { - "operationId": "getUser", - "summary": "Get a user", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 1 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } + "$ref": "file://./paths/users/userID/get.json" }, "put": { - "operationId": "updateUser", - "summary": "Update a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 2, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } - } - }, - "schema": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } + "$ref": "file://./paths/users/userID/put.json" }, "delete": { - "operationId": "deleteUser", - "summary": "Delete a User", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } + "$ref": "file://./paths/users/userID/delete.json" } }, "/users/{userID}/auth": { - "put": { - "operationId": "updateUserAuth", - "summary": "Update a User's Authentication", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "oneOf": [ - { - "type": "string", - "pattern": "^me$" - }, - { - "type": "integer", - "minimum": 1 - } - ] - }, - "required": true, - "description": "User ID or 'me' for yourself", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "User Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/AuthObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/permissions": { - "put": { - "operationId": "updateUserPermissions", - "summary": "Update a User's Permissions", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - }, - { - "in": "body", - "name": "user", - "description": "Permissions Payload", - "required": true, - "schema": { - "$ref": "#/components/schemas/PermissionsObject" - } - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": true - } - }, - "schema": { - "type": "boolean" - } - } - } - } - } - } - }, - "/users/{userID}/login": { - "put": { - "operationId": "loginAsUser", - "summary": "Login as this user", - "tags": [ - "Users" - ], - "security": [ - { - "BearerAuth": [ - "users" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "userID", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "description": "User ID", - "example": 2 - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg", - "expires": "2020-01-31T10:56:23.239Z", - "user": { - "id": 1, - "created_on": "2020-01-30T10:43:44.000Z", - "modified_on": "2020-01-30T10:43:44.000Z", - "is_disabled": 0, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "nickname": "James", - "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", - "roles": [ - "admin" - ] - } - } - } - }, - "schema": { - "type": "object", - "description": "Login object", - "required": [ - "expires", - "token", - "user" - ], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - }, - "user": { - "$ref": "#/components/schemas/UserObject" - } - } - } - } - } - } - } - } - }, - "/reports/hosts": { - "get": { - "operationId": "reportsHosts", - "summary": "Report on Host Statistics", - "tags": [ - "Reports" - ], - "security": [ - { - "BearerAuth": [ - "reports" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } - } - }, - "/audit-log": { - "get": { - "operationId": "getAuditLog", - "summary": "Get Audit Log", - "tags": [ - "Audit Log" - ], - "security": [ - { - "BearerAuth": [ - "audit-log" - ] - } - ], - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "proxy": 20, - "redirection": 1, - "stream": 0, - "dead": 1 - } - } - }, - "schema": { - "$ref": "#/components/schemas/HostReportObject" - } - } - } - } - } + "post": { + "$ref": "file://./paths/users/userID/auth/post.json" } } }, "components": { - "securitySchemes": { - "BearerAuth": { - "type": "http", - "scheme": "bearer" - } - }, "schemas": { - "HealthObject": { - "type": "object", - "description": "Health object", - "additionalProperties": false, - "required": [ - "status", - "version" - ], - "properties": { - "status": { - "type": "string", - "description": "Healthy", - "example": "OK" - }, - "version": { - "type": "object", - "description": "The version object", - "example": { - "major": 2, - "minor": 0, - "revision": 0 - }, - "additionalProperties": false, - "required": [ - "major", - "minor", - "revision" - ], - "properties": { - "major": { - "type": "integer", - "minimum": 0 - }, - "minor": { - "type": "integer", - "minimum": 0 - }, - "revision": { - "type": "integer", - "minimum": 0 - } - } - } - } + "CertificateAuthorityList": { + "$ref": "file://./components/CertificateAuthorityList.json" }, - "TokenObject": { - "type": "object", - "description": "Token object", - "required": [ - "expires", - "token" - ], - "additionalProperties": false, - "properties": { - "expires": { - "description": "Token Expiry Unix Time", - "example": 1566540249, - "minimum": 1, - "type": "number" - }, - "token": { - "description": "JWT Token", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", - "type": "string" - } - } + "CertificateAuthorityObject": { + "$ref": "file://./components/CertificateAuthorityObject.json" + }, + "CertificateList": { + "$ref": "file://./components/CertificateList.json" + }, + "CertificateObject": { + "$ref": "file://./components/CertificateObject.json" + }, + "ConfigObject": { + "$ref": "file://./components/ConfigObject.json" + }, + "DeletedItemResponse": { + "$ref": "file://./components/DeletedItemResponse.json" + }, + "DNSProviderList": { + "$ref": "file://./components/DNSProviderList.json" + }, + "DNSProviderObject": { + "$ref": "file://./components/DNSProviderObject.json" + }, + "ErrorObject": { + "$ref": "file://./components/ErrorObject.json" + }, + "FilterObject": { + "$ref": "file://./components/FilterObject.json" + }, + "HealthObject": { + "$ref": "file://./components/HealthObject.json" + }, + "HostList": { + "$ref": "file://./components/HostList.json" + }, + "HostObject": { + "$ref": "file://./components/HostObject.json" + }, + "SettingList": { + "$ref": "file://./components/SettingList.json" }, "SettingObject": { - "type": "object", - "description": "Setting object", - "required": [ - "id", - "name", - "description", - "value", - "meta" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "Setting ID", - "minLength": 1, - "example": "default-site" - }, - "name": { - "type": "string", - "description": "Setting Display Name", - "minLength": 1, - "example": "Default Site" - }, - "description": { - "type": "string", - "description": "Meaningful description", - "minLength": 1, - "example": "What to show when Nginx is hit with an unknown Host" - }, - "value": { - "description": "Value in almost any form", - "example": "congratulations", - "oneOf": [ - { - "type": "string", - "minLength": 1 - }, - { - "type": "integer" - }, - { - "type": "object" - }, - { - "type": "number" - }, - { - "type": "array" - } - ] - }, - "meta": { - "description": "Extra metadata", - "example": {}, - "type": "object" - } - } + "$ref": "file://./components/SettingObject.json" }, - "SettingsList": { - "type": "array", - "description": "Setting list", - "items": { - "$ref": "#/components/schemas/SettingObject" - } + "SortObject": { + "$ref": "file://./components/SortObject.json" + }, + "StreamList": { + "$ref": "file://./components/StreamList.json" + }, + "StreamObject": { + "$ref": "file://./components/StreamObject.json" + }, + "TokenObject": { + "$ref": "file://./components/TokenObject.json" + }, + "UserList": { + "$ref": "file://./components/UserList.json" }, "UserObject": { - "type": "object", - "description": "User object", - "required": [ - "id", - "created_on", - "modified_on", - "is_disabled", - "email", - "name", - "nickname", - "avatar", - "roles" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": "integer", - "description": "User ID", - "minimum": 1, - "example": 1 - }, - "created_on": { - "type": "string", - "description": "Created Date", - "example": "2020-01-30T09:36:08.000Z" - }, - "modified_on": { - "type": "string", - "description": "Modified Date", - "example": "2020-01-30T09:41:04.000Z" - }, - "is_disabled": { - "type": "integer", - "minimum": 0, - "maximum": 1, - "description": "Is user Disabled (0 = false, 1 = true)", - "example": 0 - }, - "email": { - "type": "string", - "description": "Email", - "minLength": 3, - "example": "jc@jc21.com" - }, - "name": { - "type": "string", - "description": "Name", - "minLength": 1, - "example": "Jamie Curnow" - }, - "nickname": { - "type": "string", - "description": "Nickname", - "example": "James" - }, - "avatar": { - "type": "string", - "description": "Gravatar URL based on email, without scheme", - "example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm" - }, - "roles": { - "description": "Roles applied", - "example": [ - "admin" - ], - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UsersList": { - "type": "array", - "description": "User list", - "items": { - "$ref": "#/components/schemas/UserObject" - } - }, - "AuthObject": { - "type": "object", - "description": "Authentication Object", - "required": [ - "type", - "secret" - ], - "properties": { - "type": { - "type": "string", - "pattern": "^password$", - "example": "password" - }, - "current": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "example": "changeme" - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 64, - "example": "mySuperN3wP@ssword!" - } - } - }, - "PermissionsObject": { - "type": "object", - "properties": { - "visibility": { - "type": "string", - "description": "Visibility Type", - "enum": [ - "all", - "user" - ] - }, - "access_lists": { - "type": "string", - "description": "Access Lists Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "dead_hosts": { - "type": "string", - "description": "404 Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "proxy_hosts": { - "type": "string", - "description": "Proxy Hosts Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "redirection_hosts": { - "type": "string", - "description": "Redirection Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "streams": { - "type": "string", - "description": "Streams Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - }, - "certificates": { - "type": "string", - "description": "Certificates Permissions", - "enum": [ - "hidden", - "view", - "manage" - ] - } - } - }, - "HostReportObject": { - "type": "object", - "properties": { - "proxy": { - "type": "integer", - "description": "Proxy Hosts Count" - }, - "redirection": { - "type": "integer", - "description": "Redirection Hosts Count" - }, - "stream": { - "type": "integer", - "description": "Streams Count" - }, - "dead": { - "type": "integer", - "description": "404 Hosts Count" - } - } + "$ref": "file://./components/UserObject.json" } } } -} \ No newline at end of file +} diff --git a/backend/doc/components/CertificateAuthorityList.json b/backend/doc/components/CertificateAuthorityList.json new file mode 100644 index 00000000..73c6db58 --- /dev/null +++ b/backend/doc/components/CertificateAuthorityList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "CertificateAuthorityList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/CertificateAuthorityObject.json b/backend/doc/components/CertificateAuthorityObject.json new file mode 100644 index 00000000..62289cce --- /dev/null +++ b/backend/doc/components/CertificateAuthorityObject.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "description": "CertificateAuthorityObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "name", + "acme2_url" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "acme2_url": { + "type": "string", + "minLength": 8, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/backend/doc/components/CertificateList.json b/backend/doc/components/CertificateList.json new file mode 100644 index 00000000..54b62d69 --- /dev/null +++ b/backend/doc/components/CertificateList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "CertificateList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/CertificateObject.json b/backend/doc/components/CertificateObject.json new file mode 100644 index 00000000..abf36513 --- /dev/null +++ b/backend/doc/components/CertificateObject.json @@ -0,0 +1,82 @@ +{ + "type": "object", + "description": "CertificateObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "type", + "user_id", + "certificate_authority_id", + "dns_provider_id", + "name", + "status", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1, + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "custom", + "http", + "dns" + ] + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "certificate_authority_id": { + "type": "integer", + "minimum": 0 + }, + "dns_provider_id": { + "type": "integer", + "minimum": 0 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + }, + "status": { + "type": "string", + "enum": [ + "ready", + "requesting", + "failed", + "provided" + ] + }, + "error_message": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/backend/doc/components/ConfigObject.json b/backend/doc/components/ConfigObject.json new file mode 100644 index 00000000..644e4a4c --- /dev/null +++ b/backend/doc/components/ConfigObject.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "description": "ConfigObject" +} \ No newline at end of file diff --git a/backend/doc/components/DNSProviderList.json b/backend/doc/components/DNSProviderList.json new file mode 100644 index 00000000..2186fbb3 --- /dev/null +++ b/backend/doc/components/DNSProviderList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "DNSProviderList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/DNSProviderObject.json b/backend/doc/components/DNSProviderObject.json new file mode 100644 index 00000000..ddad518c --- /dev/null +++ b/backend/doc/components/DNSProviderObject.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "DNSProviderObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "user_id", + "provider_key", + "name", + "meta" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "provider_key": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "meta": { + "type": "object" + } + } +} \ No newline at end of file diff --git a/backend/doc/components/DeletedItemResponse.json b/backend/doc/components/DeletedItemResponse.json new file mode 100644 index 00000000..34478bc5 --- /dev/null +++ b/backend/doc/components/DeletedItemResponse.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "DeletedItemResponse", + "additionalProperties": false, + "required": [ + "result" + ], + "properties": { + "result": { + "type": "boolean", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } +} diff --git a/backend/doc/components/ErrorObject.json b/backend/doc/components/ErrorObject.json new file mode 100644 index 00000000..b6cfb338 --- /dev/null +++ b/backend/doc/components/ErrorObject.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "description": "ErrorObject", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "description": "Error code", + "minimum": 0 + }, + "message": { + "type": "string", + "description": "Error message" + } + } +} \ No newline at end of file diff --git a/backend/doc/components/FilterObject.json b/backend/doc/components/FilterObject.json new file mode 100644 index 00000000..a7ef4828 --- /dev/null +++ b/backend/doc/components/FilterObject.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "description": "FilterObject", + "additionalProperties": false, + "required": [ + "field", + "modifier", + "value" + ], + "properties": { + "field": { + "type": "string", + "description": "Field to filter with" + }, + "modifier": { + "type": "string", + "description": "Filter modifier", + "pattern": "^(equals|not|min|max|greater|lesser|contains|starts|ends|in|notin)$" + }, + "value": { + "type": "array", + "description": "Values used for filtering", + "items": { + "type": "string" + } + } + } +} diff --git a/backend/doc/components/HealthObject.json b/backend/doc/components/HealthObject.json new file mode 100644 index 00000000..c75274a7 --- /dev/null +++ b/backend/doc/components/HealthObject.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "description": "HealthObject", + "additionalProperties": false, + "required": [ + "version", + "commit", + "healthy", + "setup", + "error_reporting" + ], + "properties": { + "version": { + "type": "string", + "description": "Version", + "example": "3.0.0", + "minLength": 1 + }, + "commit": { + "type": "string", + "description": "Commit hash", + "example": "946b88f", + "minLength": 7 + }, + "healthy": { + "type": "boolean", + "description": "Healthy?", + "example": true + }, + "setup": { + "type": "boolean", + "description": "Is the application set up?", + "example": true + }, + "error_reporting": { + "type": "boolean", + "description": "Will the application send any error reporting?", + "example": true + } + } +} \ No newline at end of file diff --git a/backend/doc/components/HostList.json b/backend/doc/components/HostList.json new file mode 100644 index 00000000..fc40b33b --- /dev/null +++ b/backend/doc/components/HostList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "HostList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/HostObject.json b/backend/doc/components/HostObject.json new file mode 100644 index 00000000..6e78a40b --- /dev/null +++ b/backend/doc/components/HostObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "HostObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "provider": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/SettingList.json b/backend/doc/components/SettingList.json new file mode 100644 index 00000000..bea224b2 --- /dev/null +++ b/backend/doc/components/SettingList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "SettingList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SettingObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/SettingObject.json b/backend/doc/components/SettingObject.json new file mode 100644 index 00000000..a7c6e055 --- /dev/null +++ b/backend/doc/components/SettingObject.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "SettingObject", + "additionalProperties": false, + "required": [ + "id", + "name", + "value" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "value": { + "oneOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "integer" + } + ] + } + } +} \ No newline at end of file diff --git a/backend/doc/components/SortObject.json b/backend/doc/components/SortObject.json new file mode 100644 index 00000000..b0e0ddc6 --- /dev/null +++ b/backend/doc/components/SortObject.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "description": "SortObject", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string", + "description": "Field for sorting on" + }, + "direction": { + "type": "string", + "description": "Sort order", + "pattern": "^(ASC|DESC)$" + } + } +} diff --git a/backend/doc/components/StreamList.json b/backend/doc/components/StreamList.json new file mode 100644 index 00000000..3f7be7f4 --- /dev/null +++ b/backend/doc/components/StreamList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "StreamList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StreamObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/StreamObject.json b/backend/doc/components/StreamObject.json new file mode 100644 index 00000000..de48bed0 --- /dev/null +++ b/backend/doc/components/StreamObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "StreamObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "expires_on", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "expires_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "provider": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "domain_names": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 4 + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/TokenObject.json b/backend/doc/components/TokenObject.json new file mode 100644 index 00000000..3d2d9573 --- /dev/null +++ b/backend/doc/components/TokenObject.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "description": "TokenObject", + "additionalProperties": false, + "required": [ + "expires", + "token" + ], + "properties": { + "expires": { + "type": "number", + "description": "Token Expiry Unix Time", + "example": 1566540249, + "minimum": 1 + }, + "token": { + "type": "string", + "description": "JWT Token", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } +} \ No newline at end of file diff --git a/backend/doc/components/UserList.json b/backend/doc/components/UserList.json new file mode 100644 index 00000000..f4a624f4 --- /dev/null +++ b/backend/doc/components/UserList.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "description": "UserList", + "additionalProperties": false, + "required": [ + "total", + "offset", + "limit", + "sort" + ], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserObject" + } + } + } +} \ No newline at end of file diff --git a/backend/doc/components/UserObject.json b/backend/doc/components/UserObject.json new file mode 100644 index 00000000..7cc4aed8 --- /dev/null +++ b/backend/doc/components/UserObject.json @@ -0,0 +1,75 @@ +{ + "type": "object", + "description": "UserObject", + "additionalProperties": false, + "required": [ + "id", + "name", + "nickname", + "email", + "created_on", + "modified_on", + "roles", + "is_disabled" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "nickname": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "email": { + "type": "string", + "minLength": 5, + "maxLength": 150 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "minLength": 2 + } + }, + "gravatar_url": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + }, + "is_deleted": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "pattern": "^password$" + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/main.go b/backend/doc/main.go new file mode 100644 index 00000000..95756af7 --- /dev/null +++ b/backend/doc/main.go @@ -0,0 +1,9 @@ +package doc + +import "embed" + +// SwaggerFiles contain all the files used for swagger schema generation +//go:embed api.swagger.json +//go:embed components +//go:embed paths +var SwaggerFiles embed.FS diff --git a/backend/doc/paths/certificates-authorities/caID/delete.json b/backend/doc/paths/certificates-authorities/caID/delete.json new file mode 100644 index 00000000..3ae3bea8 --- /dev/null +++ b/backend/doc/paths/certificates-authorities/caID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteCertificateAuthority", + "summary": "Delete a Certificate Authority", + "tags": [ + "Certificate Authorities" + ], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Certificate Authority", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates-authorities/caID/get.json b/backend/doc/paths/certificates-authorities/caID/get.json new file mode 100644 index 00000000..33f35416 --- /dev/null +++ b/backend/doc/paths/certificates-authorities/caID/get.json @@ -0,0 +1,52 @@ +{ + "operationId": "getCertificateAuthority", + "summary": "Get a Certificate Authority object by ID", + "tags": [ + "Certificate Authorities" + ], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Certificate Authority", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602588511, + "modified_on": 1602588511, + "name": "Let's Encrypt", + "acme2_url": "https://acme-v02.api.letsencrypt.org/directory" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates-authorities/caID/put.json b/backend/doc/paths/certificates-authorities/caID/put.json new file mode 100644 index 00000000..b1e99602 --- /dev/null +++ b/backend/doc/paths/certificates-authorities/caID/put.json @@ -0,0 +1,61 @@ +{ + "operationId": "updateCertificateAuthority", + "summary": "Update an existing Certificate Authority", + "tags": [ + "Certificate Authorities" + ], + "parameters": [ + { + "in": "path", + "name": "caID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Certificate Authority", + "example": 1 + } + ], + "requestBody": { + "description": "Certificate Authority details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateCertificateAuthority}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602588511, + "modified_on": 1602588511, + "name": "Let's Encrypt", + "acme2_url": "https://acme-v02.api.letsencrypt.org/directory" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates-authorities/get.json b/backend/doc/paths/certificates-authorities/get.json new file mode 100644 index 00000000..14e54070 --- /dev/null +++ b/backend/doc/paths/certificates-authorities/get.json @@ -0,0 +1,88 @@ +{ + "operationId": "getCertificateAuthorities", + "summary": "Get a list of Certificate Authorities", + "tags": [ + "Certificate Authorities" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 2, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1602588511, + "modified_on": 1602588511, + "name": "Let's Encrypt", + "acme2_url": "https://acme-v02.api.letsencrypt.org/directory" + }, + { + "id": 2, + "created_on": 1602588511, + "modified_on": 1602588511, + "name": "Let's Encrypt (Staging)", + "acme2_url": "https://acme-staging-v02.api.letsencrypt.org/directory" + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates-authorities/post.json b/backend/doc/paths/certificates-authorities/post.json new file mode 100644 index 00000000..8cd359db --- /dev/null +++ b/backend/doc/paths/certificates-authorities/post.json @@ -0,0 +1,48 @@ +{ + "operationId": "createCertificateAuthority", + "summary": "Create a new Certificate Authority", + "tags": [ + "Certificate Authorities" + ], + "requestBody": { + "description": "Certificate Authority to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateCertificateAuthority}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 3, + "created_on": 1602588900, + "modified_on": 1602588900, + "name": "Boulder", + "acme2_url": "https://boulder.local/directory" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates/certificateID/delete.json b/backend/doc/paths/certificates/certificateID/delete.json new file mode 100644 index 00000000..98acfaf7 --- /dev/null +++ b/backend/doc/paths/certificates/certificateID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteCertificate", + "summary": "Delete a Certificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the certificate", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a certificate that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates/certificateID/get.json b/backend/doc/paths/certificates/certificateID/get.json new file mode 100644 index 00000000..99613b67 --- /dev/null +++ b/backend/doc/paths/certificates/certificateID/get.json @@ -0,0 +1,60 @@ +{ + "operationId": "getCertificate", + "summary": "Get a certificate object by ID", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the certificate", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates/certificateID/put.json b/backend/doc/paths/certificates/certificateID/put.json new file mode 100644 index 00000000..514790eb --- /dev/null +++ b/backend/doc/paths/certificates/certificateID/put.json @@ -0,0 +1,69 @@ +{ + "operationId": "updateCertificate", + "summary": "Update an existing Certificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "path", + "name": "certificateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the certificate", + "example": 1 + } + ], + "requestBody": { + "description": "Certificate details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateCertificate}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates/get.json b/backend/doc/paths/certificates/get.json new file mode 100644 index 00000000..1a31135c --- /dev/null +++ b/backend/doc/paths/certificates/get.json @@ -0,0 +1,89 @@ +{ + "operationId": "getCertificates", + "summary": "Get a list of certificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "status": "ready" + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/certificates/post.json b/backend/doc/paths/certificates/post.json new file mode 100644 index 00000000..b4566e6c --- /dev/null +++ b/backend/doc/paths/certificates/post.json @@ -0,0 +1,56 @@ +{ + "operationId": "createCertificate", + "summary": "Create a new Certificate", + "tags": [ + "Certificates" + ], + "requestBody": { + "description": "Certificate to create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateCertificate}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1604536109, + "modified_on": 1604536109, + "expires_on": null, + "type": "dns", + "user_id": 1, + "certificate_authority_id": 2, + "dns_provider_id": 1, + "name": "test1.jc21.com.au", + "domain_names": [ + "test1.jc21.com.au" + ], + "status": "ready" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/config/get.json b/backend/doc/paths/config/get.json new file mode 100644 index 00000000..ea1f0701 --- /dev/null +++ b/backend/doc/paths/config/get.json @@ -0,0 +1,36 @@ +{ + "operationId": "config", + "summary": "Returns the API Service configuration", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/ConfigObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "data": "/data", + "log": { + "level": "debug", + "format": "nice" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/dns-providers/get.json b/backend/doc/paths/dns-providers/get.json new file mode 100644 index 00000000..19a298bf --- /dev/null +++ b/backend/doc/paths/dns-providers/get.json @@ -0,0 +1,87 @@ +{ + "operationId": "getDNSProviders", + "summary": "Get a list of DNS Providers", + "tags": [ + "DNS Providers" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "provider_key": "route53", + "name": "Route53", + "meta": { + "access_key": "abc123", + "access_secret": "def098", + "zone_id": "ABC123" + } + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/dns-providers/post.json b/backend/doc/paths/dns-providers/post.json new file mode 100644 index 00000000..35bc2bc3 --- /dev/null +++ b/backend/doc/paths/dns-providers/post.json @@ -0,0 +1,54 @@ +{ + "operationId": "createDNSProvider", + "summary": "Create a new DNS Provider", + "tags": [ + "DNS Providers" + ], + "requestBody": { + "description": "DNS Provider to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateDNSProvider}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "provider_key": "route53", + "name": "Route53", + "meta": { + "access_key": "abc123", + "access_secret": "def098", + "zone_id": "ABC123" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/dns-providers/providerID/delete.json b/backend/doc/paths/dns-providers/providerID/delete.json new file mode 100644 index 00000000..32b77b0d --- /dev/null +++ b/backend/doc/paths/dns-providers/providerID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteDNSProvider", + "summary": "Delete a DNS Provider", + "tags": [ + "DNS Providers" + ], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the DNS Provider", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a DNS Provider that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/dns-providers/providerID/get.json b/backend/doc/paths/dns-providers/providerID/get.json new file mode 100644 index 00000000..316e8d2e --- /dev/null +++ b/backend/doc/paths/dns-providers/providerID/get.json @@ -0,0 +1,58 @@ +{ + "operationId": "getDNSProvider", + "summary": "Get a DNS Provider object by ID", + "tags": [ + "DNS Providers" + ], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the DNS Provider", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "provider_key": "route53", + "name": "Route53", + "meta": { + "access_key": "abc123", + "access_secret": "def098", + "zone_id": "ABC123" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/dns-providers/providerID/put.json b/backend/doc/paths/dns-providers/providerID/put.json new file mode 100644 index 00000000..9b1d69fa --- /dev/null +++ b/backend/doc/paths/dns-providers/providerID/put.json @@ -0,0 +1,69 @@ +{ + "operationId": "updateDNSProvider", + "summary": "Update an existing DNS Provider", + "tags": [ + "DNS Providers" + ], + "parameters": [ + { + "in": "path", + "name": "providerID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the DNS Provider", + "example": 1 + } + ], + "requestBody": { + "description": "DNS Provider details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateDNSProvider}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "result": { + "id": 1, + "created_on": 1602593653, + "modified_on": 1602593653, + "user_id": 1, + "provider_key": "route53", + "name": "Route53", + "meta": { + "access_key": "abc123", + "access_secret": "def098", + "zone_id": "ABC123" + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/get.json b/backend/doc/paths/get.json new file mode 100644 index 00000000..0f9506f1 --- /dev/null +++ b/backend/doc/paths/get.json @@ -0,0 +1,47 @@ +{ + "operationId": "health", + "summary": "Returns the API health status", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HealthObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": true, + "setup": true, + "error_reporting": true + } + } + }, + "unhealthy": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": false, + "setup": true, + "error_reporting": true + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/hosts/get.json b/backend/doc/paths/hosts/get.json new file mode 100644 index 00000000..0ecac1d7 --- /dev/null +++ b/backend/doc/paths/hosts/get.json @@ -0,0 +1,75 @@ +{ + "operationId": "getHosts", + "summary": "Get a list of Hosts", + "tags": [ + "Hosts" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HostList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + "TODO" + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/hosts/hostID/delete.json b/backend/doc/paths/hosts/hostID/delete.json new file mode 100644 index 00000000..4df119ad --- /dev/null +++ b/backend/doc/paths/hosts/hostID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteHost", + "summary": "Delete a Host", + "tags": [ + "Hosts" + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Host", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a host that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/hosts/hostID/get.json b/backend/doc/paths/hosts/hostID/get.json new file mode 100644 index 00000000..a3782f66 --- /dev/null +++ b/backend/doc/paths/hosts/hostID/get.json @@ -0,0 +1,46 @@ +{ + "operationId": "getHost", + "summary": "Get a Host object by ID", + "tags": [ + "Hosts" + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/hosts/hostID/put.json b/backend/doc/paths/hosts/hostID/put.json new file mode 100644 index 00000000..5c9401c0 --- /dev/null +++ b/backend/doc/paths/hosts/hostID/put.json @@ -0,0 +1,55 @@ +{ + "operationId": "updateHost", + "summary": "Update an existing Host", + "tags": [ + "Hosts" + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host", + "example": 1 + } + ], + "requestBody": { + "description": "Host details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateHost}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/hosts/post.json b/backend/doc/paths/hosts/post.json new file mode 100644 index 00000000..b33d6696 --- /dev/null +++ b/backend/doc/paths/hosts/post.json @@ -0,0 +1,42 @@ +{ + "operationId": "createHost", + "summary": "Create a new Host", + "tags": [ + "Hosts" + ], + "requestBody": { + "description": "Host to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateHost}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/schema/get.json b/backend/doc/paths/schema/get.json new file mode 100644 index 00000000..e21ae805 --- /dev/null +++ b/backend/doc/paths/schema/get.json @@ -0,0 +1,9 @@ +{ + "operationId": "schema", + "summary": "Returns this swagger API schema", + "responses": { + "200": { + "description": "200 response" + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/settings/get.json b/backend/doc/paths/settings/get.json new file mode 100644 index 00000000..3455b0ff --- /dev/null +++ b/backend/doc/paths/settings/get.json @@ -0,0 +1,84 @@ +{ + "operationId": "getSettings", + "summary": "Get a list of settings", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1578010090, + "modified_on": 1578010095, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/settings/name/get.json b/backend/doc/paths/settings/name/get.json new file mode 100644 index 00000000..775a4737 --- /dev/null +++ b/backend/doc/paths/settings/name/get.json @@ -0,0 +1,55 @@ +{ + "operationId": "getSetting", + "summary": "Get a setting object by name", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "path", + "name": "name", + "schema": { + "type": "string", + "minLength": 2 + }, + "required": true, + "description": "Name of the setting", + "example": "default-site" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010095, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/settings/name/put.json b/backend/doc/paths/settings/name/put.json new file mode 100644 index 00000000..ee741104 --- /dev/null +++ b/backend/doc/paths/settings/name/put.json @@ -0,0 +1,64 @@ +{ + "operationId": "updateSetting", + "summary": "Update an existing Setting", + "tags": [ + "Settings" + ], + "parameters": [ + { + "in": "path", + "name": "name", + "schema": { + "type": "string", + "minLength": 2 + }, + "required": true, + "description": "Name of the setting", + "example": "default-site" + } + ], + "requestBody": { + "description": "Setting details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateSetting}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010090, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/settings/post.json b/backend/doc/paths/settings/post.json new file mode 100644 index 00000000..63921da2 --- /dev/null +++ b/backend/doc/paths/settings/post.json @@ -0,0 +1,51 @@ +{ + "operationId": "createSetting", + "summary": "Create a new Setting", + "tags": [ + "Settings" + ], + "requestBody": { + "description": "Setting to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateSetting}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_on": 1578010090, + "modified_on": 1578010090, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/streams/get.json b/backend/doc/paths/streams/get.json new file mode 100644 index 00000000..482212d1 --- /dev/null +++ b/backend/doc/paths/streams/get.json @@ -0,0 +1,75 @@ +{ + "operationId": "getStreams", + "summary": "Get a list of Streams", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + "TODO" + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/streams/post.json b/backend/doc/paths/streams/post.json new file mode 100644 index 00000000..d1949830 --- /dev/null +++ b/backend/doc/paths/streams/post.json @@ -0,0 +1,42 @@ +{ + "operationId": "createStream", + "summary": "Create a new Stream", + "tags": [ + "Streams" + ], + "requestBody": { + "description": "Host to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateStream}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/streams/streamID/delete.json b/backend/doc/paths/streams/streamID/delete.json new file mode 100644 index 00000000..d0f35269 --- /dev/null +++ b/backend/doc/paths/streams/streamID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteStream", + "summary": "Delete a Stream", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Stream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a Stream that is in use!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/streams/streamID/get.json b/backend/doc/paths/streams/streamID/get.json new file mode 100644 index 00000000..94e54c3f --- /dev/null +++ b/backend/doc/paths/streams/streamID/get.json @@ -0,0 +1,46 @@ +{ + "operationId": "getStream", + "summary": "Get a Stream object by ID", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Stream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/streams/streamID/put.json b/backend/doc/paths/streams/streamID/put.json new file mode 100644 index 00000000..4baa882e --- /dev/null +++ b/backend/doc/paths/streams/streamID/put.json @@ -0,0 +1,55 @@ +{ + "operationId": "updateStream", + "summary": "Update an existing Stream", + "tags": [ + "Streams" + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Stream", + "example": 1 + } + ], + "requestBody": { + "description": "Stream details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateStream}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/tokens/get.json b/backend/doc/paths/tokens/get.json new file mode 100644 index 00000000..601697a7 --- /dev/null +++ b/backend/doc/paths/tokens/get.json @@ -0,0 +1,37 @@ +{ + "operationId": "refreshToken", + "summary": "Refresh your access token", + "tags": [ + "Tokens" + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/tokens/post.json b/backend/doc/paths/tokens/post.json new file mode 100644 index 00000000..44b95b2b --- /dev/null +++ b/backend/doc/paths/tokens/post.json @@ -0,0 +1,79 @@ +{ + "operationId": "requestToken", + "summary": "Request a new access token from credentials", + "tags": [ + "Tokens" + ], + "requestBody": { + "description": "Credentials Payload", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.GetToken}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + }, + "403": { + "description": "403 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "error" + ], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 403, + "message": "Not available during setup phase" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/get.json b/backend/doc/paths/users/get.json new file mode 100644 index 00000000..45ee7efb --- /dev/null +++ b/backend/doc/paths/users/get.json @@ -0,0 +1,121 @@ +{ + "operationId": "getUsers", + "summary": "Get a list of users", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "name,nickname.desc,email.asc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/UserList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 3, + "offset": 0, + "limit": 100, + "sort": [ + { + "field": "name", + "direction": "ASC" + }, + { + "field": "nickname", + "direction": "DESC" + }, + { + "field": "email", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010090, + "modified_on": 1578010095, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false + }, + { + "id": 2, + "name": "John Doe", + "nickname": "John", + "email": "johdoe@example.com", + "created_on": 1578010100, + "modified_on": 1578010105, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false + }, + { + "id": 3, + "name": "Jane Doe", + "nickname": "Jane", + "email": "janedoe@example.com", + "created_on": 1578010110, + "modified_on": 1578010115, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/post.json b/backend/doc/paths/users/post.json new file mode 100644 index 00000000..3a524db7 --- /dev/null +++ b/backend/doc/paths/users/post.json @@ -0,0 +1,88 @@ +{ + "operationId": "createUser", + "summary": "Create a new User", + "tags": [ + "Users" + ], + "requestBody": { + "description": "User to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateUser}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010100, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "auth": { + "id": 1, + "type": "password" + } + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "required": [ + "error" + ], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "An user already exists with this email address" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/userID/auth/post.json b/backend/doc/paths/users/userID/auth/post.json new file mode 100644 index 00000000..08f12f63 --- /dev/null +++ b/backend/doc/paths/users/userID/auth/post.json @@ -0,0 +1,63 @@ +{ + "operationId": "setPassword", + "summary": "Set a User's password", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to get yourself", + "example": 1 + } + ], + "requestBody": { + "description": "Credentials to set", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.SetAuth}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "type": "string" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/userID/delete.json b/backend/doc/paths/users/userID/delete.json new file mode 100644 index 00000000..0ffa7024 --- /dev/null +++ b/backend/doc/paths/users/userID/delete.json @@ -0,0 +1,60 @@ +{ + "operationId": "deleteUser", + "summary": "Delete a User", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the user", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete yourself!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/userID/get.json b/backend/doc/paths/users/userID/get.json new file mode 100644 index 00000000..df4b3078 --- /dev/null +++ b/backend/doc/paths/users/userID/get.json @@ -0,0 +1,66 @@ +{ + "operationId": "getUser", + "summary": "Get a user object by ID or 'me'", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to get yourself", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010105, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/doc/paths/users/userID/put.json b/backend/doc/paths/users/userID/put.json new file mode 100644 index 00000000..7abb5c1d --- /dev/null +++ b/backend/doc/paths/users/userID/put.json @@ -0,0 +1,119 @@ +{ + "operationId": "updateUser", + "summary": "Update an existing User", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^me$" + } + ] + }, + "required": true, + "description": "Numeric ID of the user or 'me' to update yourself", + "example": 1 + } + ], + "requestBody": { + "description": "User details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateUser}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": [ + "result" + ], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "nickname": "James", + "email": "jc@jc21.com", + "created_on": 1578010100, + "modified_on": 1578010110, + "roles": [ + "admin" + ], + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "auth": { + "id": 1, + "type": "password" + } + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "required": [ + "error" + ], + "properties": { + "result": { + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "duplicateemail": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "A user already exists with this email address" + } + } + }, + "nodisable": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot disable yourself!" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..1b004b77 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,20 @@ +module npm + +go 1.16 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e + github.com/fatih/color v1.10.0 + github.com/getsentry/sentry-go v0.10.0 + github.com/go-chi/chi v4.1.2+incompatible + github.com/go-chi/cors v1.2.0 + github.com/go-chi/jwtauth v4.0.4+incompatible + github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 + github.com/jmoiron/sqlx v1.3.3 + github.com/mattn/go-sqlite3 v2.0.3+incompatible + github.com/qri-io/jsonschema v0.2.1 + github.com/stretchr/testify v1.7.0 + github.com/vrischmann/envconfig v1.3.0 + golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..9b1d959d --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,265 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e h1:2R8DvYLNr5DL25eWwpOdPno1eIbTNjJC0d7v8ti5cus= +github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e/go.mod h1:YjikoytuRI4q+GRd3xrOrKJN+Ayi2dwRomHLDDeMHfs= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= +github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE= +github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY= +github.com/go-chi/jwtauth v4.0.4+incompatible/go.mod h1:Q5EIArY/QnD6BdS+IyDw7B2m6iNbnPxtfd6/BcmtWbs= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jc21/jsref v0.0.0-20210608013137-43b07c7d31bd h1:Ag/L5Yc9BeBbi4i8bNAev8Ejtu/jq8Qk/xK+HDHnWNc= +github.com/jc21/jsref v0.0.0-20210608013137-43b07c7d31bd/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608014024-8bda7cb41eef h1:1jF5nv8PmgH2txfWGmsPium0Hj9PEnGkb96tkZ+4uDU= +github.com/jc21/jsref v0.0.0-20210608014024-8bda7cb41eef/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608014914-2edd4dea9791 h1:s0hsMFnTiGGytgwDbHo20OvmJj2/+FFMZvLpRNexnvk= +github.com/jc21/jsref v0.0.0-20210608014914-2edd4dea9791/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608023003-123d7fb98643 h1:ZpDTP4ow7hZMx0ORi06jnLP4ZDGQVa6SayH+5rWWlYg= +github.com/jc21/jsref v0.0.0-20210608023003-123d7fb98643/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608023437-810a57e5f736 h1:1nZYRLsHvECy8rbOLkqRBK45Y6zKQ5ZRuGPMQalPWVc= +github.com/jc21/jsref v0.0.0-20210608023437-810a57e5f736/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608024103-9eaa65f76123 h1:pb24Ybg78OdqO4GHh0xcwlVPWKlDYX/ZVnf+wq8D9To= +github.com/jc21/jsref v0.0.0-20210608024103-9eaa65f76123/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k= +github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kyoh86/richgo v0.3.8/go.mod h1:2C8POkF1H04iTOG2Tp1yyZhspCME9nN3cir3VXJ02II= +github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c= +github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8= +github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4= +github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b h1:YUFRoeHK/mvRjBR0bBRDC7ZGygYchoQ8j1xMENlObro= +github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b/go.mod h1:s2U6PowV3/Jobkx/S9d0XiPwOzs6niW3DIouw+7nZC8= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= +github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= +github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0= +github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= +github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= +github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index f4f79518..00000000 --- a/backend/index.js +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env node - -const logger = require('./logger').global; - -async function appStart () { - // Create config file db settings if environment variables have been set - await createDbConfigFromEnvironment(); - - const migrate = require('./migrate'); - const setup = require('./setup'); - const app = require('./app'); - const apiValidator = require('./lib/validator/api'); - const internalCertificate = require('./internal/certificate'); - const internalIpRanges = require('./internal/ip_ranges'); - - return migrate.latest() - .then(setup) - .then(() => { - return apiValidator.loadSchemas; - }) - .then(internalIpRanges.fetch) - .then(() => { - - internalCertificate.initTimer(); - internalIpRanges.initTimer(); - - const server = app.listen(3000, () => { - logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...'); - - process.on('SIGTERM', () => { - logger.info('PID ' + process.pid + ' received SIGTERM'); - server.close(() => { - logger.info('Stopping.'); - process.exit(0); - }); - }); - }); - }) - .catch((err) => { - logger.error(err.message); - setTimeout(appStart, 1000); - }); -} - -async function createDbConfigFromEnvironment() { - return new Promise((resolve, reject) => { - const envMysqlHost = process.env.DB_MYSQL_HOST || null; - const envMysqlPort = process.env.DB_MYSQL_PORT || null; - const envMysqlUser = process.env.DB_MYSQL_USER || null; - const envMysqlName = process.env.DB_MYSQL_NAME || null; - const envSqliteFile = process.env.DB_SQLITE_FILE || null; - - if ((envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) || envSqliteFile) { - const fs = require('fs'); - const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json'; - let configData = {}; - - try { - configData = require(filename); - } catch (err) { - // do nothing - } - - if (configData.database && configData.database.engine && !configData.database.fromEnv) { - logger.info('Manual db configuration already exists, skipping config creation from environment variables'); - resolve(); - return; - } - - if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) { - const newConfig = { - fromEnv: true, - engine: 'mysql', - host: envMysqlHost, - port: envMysqlPort, - user: envMysqlUser, - password: process.env.DB_MYSQL_PASSWORD, - name: envMysqlName, - }; - - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating MySQL db configuration from environment variables'); - configData.database = newConfig; - - } else { - const newConfig = { - fromEnv: true, - engine: 'knex-native', - knex: { - client: 'sqlite3', - connection: { - filename: envSqliteFile - }, - useNullAsDefault: true - } - }; - if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) { - // Config is unchanged, skip overwrite - resolve(); - return; - } - - logger.info('Generating Sqlite db configuration from environment variables'); - configData.database = newConfig; - } - - // Write config - fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => { - if (err) { - logger.error('Could not write db config to config file: ' + filename); - reject(err); - } else { - logger.info('Wrote db configuration to config file: ' + filename); - resolve(); - } - }); - } else { - resolve(); - } - }); -} - -try { - appStart(); -} catch (err) { - logger.error(err.message, err); - process.exit(1); -} - diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js deleted file mode 100644 index 5b817d03..00000000 --- a/backend/internal/access-list.js +++ /dev/null @@ -1,534 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const batchflow = require('batchflow'); -const logger = require('../logger').access; -const error = require('../lib/error'); -const accessListModel = require('../models/access_list'); -const accessListAuthModel = require('../models/access_list_auth'); -const accessListClientModel = require('../models/access_list_client'); -const proxyHostModel = require('../models/proxy_host'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const utils = require('../lib/utils'); - -function omissions () { - return ['is_deleted']; -} - -const internalAccessList = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - return access.can('access_lists:create', data) - .then((/*access_data*/) => { - return accessListModel - .query() - .omit(omissions()) - .insertAndFetch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - owner_user_id: access.token.getUserId(1) - }); - }) - .then((row) => { - data.id = row.id; - - let promises = []; - - // Now add the items - data.items.map((item) => { - promises.push(accessListAuthModel - .query() - .insert({ - access_list_id: row.id, - username: item.username, - password: item.password - }) - ); - }); - - // Now add the clients - if (typeof data.clients !== 'undefined' && data.clients) { - data.clients.map((client) => { - promises.push(accessListClientModel - .query() - .insert({ - access_list_id: row.id, - address: client.address, - directive: client.directive - }) - ); - }); - } - - return Promise.all(promises); - }) - .then(() => { - // re-fetch with expansions - return internalAccessList.get(access, { - id: data.id, - expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] - }, true /* <- skip masking */); - }) - .then((row) => { - // Audit log - data.meta = _.assign({}, data.meta || {}, row.meta); - - return internalAccessList.build(row) - .then(() => { - if (row.proxy_host_count) { - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - } - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'access-list', - object_id: row.id, - meta: internalAccessList.maskItems(data) - }); - }) - .then(() => { - return internalAccessList.maskItems(row); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.name] - * @param {String} [data.items] - * @return {Promise} - */ - update: (access, data) => { - return access.can('access_lists:update', data.id) - .then((/*access_data*/) => { - return internalAccessList.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - }) - .then(() => { - // patch name if specified - if (typeof data.name !== 'undefined' && data.name) { - return accessListModel - .query() - .where({id: data.id}) - .patch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - }); - } - }) - .then(() => { - // Check for items and add/update/remove them - if (typeof data.items !== 'undefined' && data.items) { - let promises = []; - let items_to_keep = []; - - data.items.map(function (item) { - if (item.password) { - promises.push(accessListAuthModel - .query() - .insert({ - access_list_id: data.id, - username: item.username, - password: item.password - }) - ); - } else { - // This was supplied with an empty password, which means keep it but don't change the password - items_to_keep.push(item.username); - } - }); - - let query = accessListAuthModel - .query() - .delete() - .where('access_list_id', data.id); - - if (items_to_keep.length) { - query.andWhere('username', 'NOT IN', items_to_keep); - } - - return query - .then(() => { - // Add new items - if (promises.length) { - return Promise.all(promises); - } - }); - } - }) - .then(() => { - // Check for clients and add/update/remove them - if (typeof data.clients !== 'undefined' && data.clients) { - let promises = []; - - data.clients.map(function (client) { - if (client.address) { - promises.push(accessListClientModel - .query() - .insert({ - access_list_id: data.id, - address: client.address, - directive: client.directive - }) - ); - } - }); - - let query = accessListClientModel - .query() - .delete() - .where('access_list_id', data.id); - - return query - .then(() => { - // Add new items - if (promises.length) { - return Promise.all(promises); - } - }); - } - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'access-list', - object_id: data.id, - meta: internalAccessList.maskItems(data) - }); - }) - .then(() => { - // re-fetch with expansions - return internalAccessList.get(access, { - id: data.id, - expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]'] - }, true /* <- skip masking */); - }) - .then((row) => { - return internalAccessList.build(row) - .then(() => { - if (row.proxy_host_count) { - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - } - }) - .then(() => { - return internalAccessList.maskItems(row); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @param {Boolean} [skip_masking] - * @return {Promise} - */ - get: (access, data, skip_masking) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('access_lists:get', data.id) - .then((access_data) => { - let query = accessListModel - .query() - .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) - .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') - .where('access_list.is_deleted', 0) - .andWhere('access_list.id', data.id) - .allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]') - .omit(['access_list.is_deleted']) - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - if (!skip_masking && typeof row.items !== 'undefined' && row.items) { - row = internalAccessList.maskItems(row); - } - - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('access_lists:delete', data.id) - .then(() => { - return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - // 1. update row to be deleted - // 2. update any proxy hosts that were using it (ignoring permissions) - // 3. reconfigure those hosts - // 4. audit log - - // 1. update row to be deleted - return accessListModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // 2. update any proxy hosts that were using it (ignoring permissions) - if (row.proxy_hosts) { - return proxyHostModel - .query() - .where('access_list_id', '=', row.id) - .patch({access_list_id: 0}) - .then(() => { - // 3. reconfigure those hosts, then reload nginx - - // set the access_list_id to zero for these items - row.proxy_hosts.map(function (val, idx) { - row.proxy_hosts[idx].access_list_id = 0; - }); - - return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); - }) - .then(() => { - return internalNginx.reload(); - }); - } - }) - .then(() => { - // delete the htpasswd file - let htpasswd_file = internalAccessList.getFilename(row); - - try { - fs.unlinkSync(htpasswd_file); - } catch (err) { - // do nothing - } - }) - .then(() => { - // 4. audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'access-list', - object_id: row.id, - meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts']) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Lists - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('access_lists:list') - .then((access_data) => { - let query = accessListModel - .query() - .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) - .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') - .where('access_list.is_deleted', 0) - .groupBy('access_list.id') - .omit(['access_list.is_deleted']) - .allowEager('[owner,items,clients]') - .orderBy('access_list.name', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .then((rows) => { - if (rows) { - rows.map(function (row, idx) { - if (typeof row.items !== 'undefined' && row.items) { - rows[idx] = internalAccessList.maskItems(row); - } - }); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Integer} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = accessListModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - }, - - /** - * @param {Object} list - * @returns {Object} - */ - maskItems: (list) => { - if (list && typeof list.items !== 'undefined') { - list.items.map(function (val, idx) { - let repeat_for = 8; - let first_char = '*'; - - if (typeof val.password !== 'undefined' && val.password) { - repeat_for = val.password.length - 1; - first_char = val.password.charAt(0); - } - - list.items[idx].hint = first_char + ('*').repeat(repeat_for); - list.items[idx].password = ''; - }); - } - - return list; - }, - - /** - * @param {Object} list - * @param {Integer} list.id - * @returns {String} - */ - getFilename: (list) => { - return '/data/access/' + list.id; - }, - - /** - * @param {Object} list - * @param {Integer} list.id - * @param {String} list.name - * @param {Array} list.items - * @returns {Promise} - */ - build: (list) => { - logger.info('Building Access file #' + list.id + ' for: ' + list.name); - - return new Promise((resolve, reject) => { - let htpasswd_file = internalAccessList.getFilename(list); - - // 1. remove any existing access file - try { - fs.unlinkSync(htpasswd_file); - } catch (err) { - // do nothing - } - - // 2. create empty access file - try { - fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'}); - resolve(htpasswd_file); - } catch (err) { - reject(err); - } - }) - .then((htpasswd_file) => { - // 3. generate password for each user - if (list.items.length) { - return new Promise((resolve, reject) => { - batchflow(list.items).sequential() - .each((i, item, next) => { - if (typeof item.password !== 'undefined' && item.password.length) { - logger.info('Adding: ' + item.username); - - utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"') - .then((/*result*/) => { - next(); - }) - .catch((err) => { - logger.error(err); - next(err); - }); - } - }) - .error((err) => { - logger.error(err); - reject(err); - }) - .end((results) => { - logger.success('Built Access file #' + list.id + ' for: ' + list.name); - resolve(results); - }); - }); - } - }); - } -}; - -module.exports = internalAccessList; diff --git a/backend/internal/api/context/context.go b/backend/internal/api/context/context.go new file mode 100644 index 00000000..f3bc957b --- /dev/null +++ b/backend/internal/api/context/context.go @@ -0,0 +1,25 @@ +package context + +var ( + // BodyCtxKey is the name of the Body value on the context + BodyCtxKey = &contextKey{"Body"} + // UserIDCtxKey is the name of the UserID value on the context + UserIDCtxKey = &contextKey{"UserID"} + // FiltersCtxKey is the name of the Filters value on the context + FiltersCtxKey = &contextKey{"Filters"} + // PrettyPrintCtxKey is the name of the pretty print context + PrettyPrintCtxKey = &contextKey{"Pretty"} + // ExpansionCtxKey is the name of the expansion context + ExpansionCtxKey = &contextKey{"Expansion"} +) + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "context value: " + k.name +} diff --git a/backend/internal/api/filters/helpers.go b/backend/internal/api/filters/helpers.go new file mode 100644 index 00000000..5f5d5238 --- /dev/null +++ b/backend/internal/api/filters/helpers.go @@ -0,0 +1,208 @@ +package filters + +import ( + "fmt" + "strings" +) + +// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware +func NewFilterSchema(fieldSchemas []string) string { + return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", ")) +} + +// BoolFieldSchema returns the Field Schema for a Boolean accepted value field +func BoolFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, boolModifiers, filterBool, filterBool) +} + +// IntFieldSchema returns the Field Schema for a Integer accepted value field +func IntFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// StringFieldSchema returns the Field Schema for a String accepted value field +func StringFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, stringModifiers, filterString, filterString) +} + +// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex +func RegexFieldSchema(fieldName string, regex string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "%s" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "%s" + } + } + ] + } + } + }`, fieldName, stringModifiers, regex, regex) +} + +// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format +func DateFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format +// 2020-03-01T10:30:00+10:00 +func DateTimeFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +const allModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$" +}` + +const boolModifiers = `{ + "type": "string", + "pattern": "^(equals|not)$" +}` + +const stringModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin)$" +}` + +const filterBool = `{ + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" +}` + +const filterString = `{ + "type": "string", + "minLength": 1 +}` + +const baseFilterSchema = `{ + "type": "array", + "items": { + "oneOf": [ + %s + ] + } +}` diff --git a/backend/internal/api/handler/auth.go b/backend/internal/api/handler/auth.go new file mode 100644 index 00000000..972022de --- /dev/null +++ b/backend/internal/api/handler/auth.go @@ -0,0 +1,54 @@ +package handler + +import ( + "encoding/json" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/entity/auth" + "npm/internal/logger" +) + +// SetAuth ... +// Route: POST /users/:userID/auth +func SetAuth() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + // TODO: + // delete old auth for user + // test endpoint + + var newAuth auth.Model + err := json.Unmarshal(bodyBytes, &newAuth) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + userID, _, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + newAuth.UserID = userID + if newAuth.Type == auth.TypePassword { + err := newAuth.SetPassword(newAuth.Secret) + if err != nil { + logger.Error("SetPasswordError", err) + } + } + + if err = newAuth.Save(); err != nil { + logger.Error("AuthSaveError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) + return + } + + newAuth.Secret = "" + + h.ResultResponseJSON(w, r, http.StatusOK, newAuth) + } +} diff --git a/backend/internal/api/handler/certificate_authorities.go b/backend/internal/api/handler/certificate_authorities.go new file mode 100644 index 00000000..e5c453ef --- /dev/null +++ b/backend/internal/api/handler/certificate_authorities.go @@ -0,0 +1,126 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/certificateauthority" +) + +// GetCertificateAuthorities will return a list of Certificate Authorities +// Route: GET /certificate-authorities +func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, certificates) + } + } +} + +// GetCertificateAuthority will return a single Certificate Authority +// Route: GET /certificate-authorities/{caID} +func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert) + } + } +} + +// CreateCertificateAuthority will create a Certificate Authority +// Route: POST /certificate-authorities +func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newCA certificateauthority.Model + err := json.Unmarshal(bodyBytes, &newCA) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newCA.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newCA) + } +} + +// UpdateCertificateAuthority ... +// Route: PUT /certificate-authorities/{caID} +func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + ca, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &ca) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = ca.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, ca) + } + } +} + +// DeleteCertificateAuthority ... +// Route: DELETE /certificate-authorities/{caID} +func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var caID int + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificateauthority.GetByID(caID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete()) + } + } +} diff --git a/backend/internal/api/handler/certificates.go b/backend/internal/api/handler/certificates.go new file mode 100644 index 00000000..8e70794b --- /dev/null +++ b/backend/internal/api/handler/certificates.go @@ -0,0 +1,145 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/api/schema" + "npm/internal/entity/certificate" +) + +// GetCertificates will return a list of Certificates +// Route: GET /certificates +func GetCertificates() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, certificates) + } + } +} + +// GetCertificate will return a single Certificate +// Route: GET /certificates/{certificateID} +func GetCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert) + } + } +} + +// CreateCertificate will create a Certificate +// Route: POST /certificates +func CreateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newCertificate certificate.Model + err := json.Unmarshal(bodyBytes, &newCertificate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newCertificate.UserID = userID + + if err = newCertificate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newCertificate) + } +} + +// UpdateCertificate ... +// Route: PUT /certificates/{certificateID} +func UpdateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + certificateObject, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + // This is a special endpoint, as it needs to verify the schema payload + // based on the certificate type, without being given a type in the payload. + // The middleware would normally handle this. + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes) + if jsonErr != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil) + return + } + + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return + } + + err := json.Unmarshal(bodyBytes, &certificateObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = certificateObject.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, certificateObject) + } + } +} + +// DeleteCertificate ... +// Route: DELETE /certificates/{certificateID} +func DeleteCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var certificateID int + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + cert, err := certificate.GetByID(certificateID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete()) + } + } +} diff --git a/backend/internal/api/handler/config.go b/backend/internal/api/handler/config.go new file mode 100644 index 00000000..811d7580 --- /dev/null +++ b/backend/internal/api/handler/config.go @@ -0,0 +1,15 @@ +package handler + +import ( + "net/http" + h "npm/internal/api/http" + "npm/internal/config" +) + +// Config returns the entire configuration, for debug purposes +// Route: GET /config +func Config() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration) + } +} diff --git a/backend/internal/api/handler/dns_providers.go b/backend/internal/api/handler/dns_providers.go new file mode 100644 index 00000000..045619aa --- /dev/null +++ b/backend/internal/api/handler/dns_providers.go @@ -0,0 +1,129 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/dnsprovider" +) + +// GetDNSProviders will return a list of DNS Providers +// Route: GET /dns-providers +func GetDNSProviders() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, items) + } + } +} + +// GetDNSProvider will return a single DNS Provider +// Route: GET /dns-providers/{providerID} +func GetDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// CreateDNSProvider will create a DNS Provider +// Route: POST /dns-providers +func CreateDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newItem dnsprovider.Model + err := json.Unmarshal(bodyBytes, &newItem) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newItem.UserID = userID + + if err = newItem.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newItem) + } +} + +// UpdateDNSProvider ... +// Route: PUT /dns-providers/{providerID} +func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &item) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = item.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// DeleteDNSProvider ... +// Route: DELETE /dns-providers/{providerID} +func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var providerID int + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + } + } +} diff --git a/backend/internal/api/handler/health.go b/backend/internal/api/handler/health.go new file mode 100644 index 00000000..6e128112 --- /dev/null +++ b/backend/internal/api/handler/health.go @@ -0,0 +1,31 @@ +package handler + +import ( + "net/http" + h "npm/internal/api/http" + "npm/internal/config" +) + +type healthCheckResponse struct { + Version string `json:"version"` + Commit string `json:"commit"` + Healthy bool `json:"healthy"` + IsSetup bool `json:"setup"` + ErrorReporting bool `json:"error_reporting"` +} + +// Health returns the health of the api +// Route: GET /health +func Health() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + health := healthCheckResponse{ + Version: config.Version, + Commit: config.Commit, + Healthy: true, + IsSetup: config.IsSetup, + ErrorReporting: config.ErrorReporting, + } + + h.ResultResponseJSON(w, r, http.StatusOK, health) + } +} diff --git a/backend/internal/api/handler/helpers.go b/backend/internal/api/handler/helpers.go new file mode 100644 index 00000000..3727269a --- /dev/null +++ b/backend/internal/api/handler/helpers.go @@ -0,0 +1,151 @@ +package handler + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "npm/internal/model" + + "github.com/go-chi/chi" +) + +const defaultLimit = 10 + +func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) { + var pageInfo model.PageInfo + var err error + + pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r) + if err != nil { + return pageInfo, err + } + + pageInfo.Offset, pageInfo.Limit, err = getPagination(r) + if err != nil { + return pageInfo, err + } + + pageInfo.Sort = getSortParameter(r) + + return pageInfo, nil +} + +func getDateRanges(r *http.Request) (time.Time, time.Time, error) { + queryValues := r.URL.Query() + from := queryValues.Get("from") + fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default + to := queryValues.Get("to") + toDate := time.Now() + + if from != "" { + var fromErr error + fromDate, fromErr = time.Parse(time.RFC3339, from) + if fromErr != nil { + return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+")) + } + } + + if to != "" { + var toErr error + toDate, toErr = time.Parse(time.RFC3339, to) + if toErr != nil { + return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+")) + } + } + + return fromDate, toDate, nil +} + +func getSortParameter(r *http.Request) []model.Sort { + var sortFields []model.Sort + + queryValues := r.URL.Query() + sortString := queryValues.Get("sort") + if sortString == "" { + return sortFields + } + + // Split sort fields up in to slice + sorts := strings.Split(sortString, ",") + for _, sortItem := range sorts { + if strings.Contains(sortItem, ".") { + theseItems := strings.Split(sortItem, ".") + + switch strings.ToLower(theseItems[1]) { + case "desc": + fallthrough + case "descending": + theseItems[1] = "DESC" + default: + theseItems[1] = "ASC" + } + + sortFields = append(sortFields, model.Sort{ + Field: theseItems[0], + Direction: theseItems[1], + }) + } else { + sortFields = append(sortFields, model.Sort{ + Field: sortItem, + Direction: "ASC", + }) + } + } + + return sortFields +} + +func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) { + queryValues := r.URL.Query() + varValue := queryValues.Get(varName) + + if varValue == "" && required { + return 0, fmt.Errorf("%v was not supplied in the request", varName) + } else if varValue == "" { + return defaultValue, nil + } + + varInt, intErr := strconv.Atoi(varValue) + if intErr != nil { + return 0, fmt.Errorf("%v is not a valid number", varName) + } + + return varInt, nil +} + +func getURLParamInt(r *http.Request, varName string) (int, error) { + required := true + defaultValue := 0 + paramStr := chi.URLParam(r, varName) + var err error + var paramInt int + + if paramStr == "" && required { + return 0, fmt.Errorf("%v was not supplied in the request", varName) + } else if paramStr == "" { + return defaultValue, nil + } + + if paramInt, err = strconv.Atoi(paramStr); err != nil { + return 0, fmt.Errorf("%v is not a valid number", varName) + } + + return paramInt, nil +} + +func getPagination(r *http.Request) (int, int, error) { + var err error + offset, err := getQueryVarInt(r, "offset", false, 0) + if err != nil { + return 0, 0, err + } + limit, err := getQueryVarInt(r, "limit", false, defaultLimit) + if err != nil { + return 0, 0, err + } + + return offset, limit, nil +} diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go new file mode 100644 index 00000000..58c75c98 --- /dev/null +++ b/backend/internal/api/handler/hosts.go @@ -0,0 +1,135 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/host" + "npm/internal/validator" +) + +// GetHosts will return a list of Hosts +// Route: GET /hosts +func GetHosts() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hosts) + } + } +} + +// GetHost will return a single Host +// Route: GET /hosts/{hostID} +func GetHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// CreateHost will create a Host +// Route: POST /hosts +func CreateHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newHost host.Model + err := json.Unmarshal(bodyBytes, &newHost) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newHost.UserID = userID + + if err = validator.ValidateHost(newHost); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newHost.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newHost) + } +} + +// UpdateHost ... +// Route: PUT /hosts/{hostID} +func UpdateHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &host) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = host.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// DeleteHost ... +// Route: DELETE /hosts/{hostID} +func DeleteHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := host.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host.Delete()) + } + } +} diff --git a/backend/internal/api/handler/not_allowed.go b/backend/internal/api/handler/not_allowed.go new file mode 100644 index 00000000..966debab --- /dev/null +++ b/backend/internal/api/handler/not_allowed.go @@ -0,0 +1,14 @@ +package handler + +import ( + "net/http" + + h "npm/internal/api/http" +) + +// NotAllowed is a json error handler for when method is not allowed +func NotAllowed() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil) + } +} diff --git a/backend/internal/api/handler/not_found.go b/backend/internal/api/handler/not_found.go new file mode 100644 index 00000000..04893e9b --- /dev/null +++ b/backend/internal/api/handler/not_found.go @@ -0,0 +1,65 @@ +package handler + +import ( + "embed" + "errors" + "io" + "io/fs" + "mime" + "net/http" + "path/filepath" + "strings" + + h "npm/internal/api/http" +) + +//go:embed assets +var assets embed.FS +var assetsSub fs.FS + +var errIsDir = errors.New("path is dir") + +// NotFound is a json error handler for 404's and method not allowed. +// It also serves the react frontend as embedded files in the golang binary. +func NotFound() func(http.ResponseWriter, *http.Request) { + assetsSub, _ = fs.Sub(assets, "assets") + + return func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimLeft(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + + err := tryRead(assetsSub, path, w) + if err == errIsDir { + err = tryRead(assetsSub, "index.html", w) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + } + } else if err == nil { + return + } + + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + } +} + +func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error { + f, err := folder.Open(requestedPath) + if err != nil { + return err + } + + // nolint: errcheck + defer f.Close() + + stat, _ := f.Stat() + if stat.IsDir() { + return errIsDir + } + + contentType := mime.TypeByExtension(filepath.Ext(requestedPath)) + w.Header().Set("Content-Type", contentType) + _, err = io.Copy(w, f) + return err +} diff --git a/backend/internal/api/handler/schema.go b/backend/internal/api/handler/schema.go new file mode 100644 index 00000000..38440f4c --- /dev/null +++ b/backend/internal/api/handler/schema.go @@ -0,0 +1,99 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "npm/doc" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/logger" + + jsref "github.com/jc21/jsref" + "github.com/jc21/jsref/provider" +) + +var swaggerSchema []byte + +// Schema simply reads the swagger schema from disk and returns is raw +// Route: GET /schema +func Schema() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(getSchema())) + } +} + +func getSchema() []byte { + if swaggerSchema == nil { + // nolint:gosec + swaggerSchema, _ = doc.SwaggerFiles.ReadFile("api.swagger.json") + + // Replace {{VERSION}} with Config Version + swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version)) + + // Dereference the JSON Schema: + var schema interface{} + if err := json.Unmarshal(swaggerSchema, &schema); err != nil { + logger.Error("SwaggerUnmarshalError", err) + return nil + } + + provider := provider.NewIoFS(doc.SwaggerFiles, "") + resolver := jsref.New() + err := resolver.AddProvider(provider) + if err != nil { + logger.Error("SchemaProviderError", err) + } + + result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...) + if err != nil { + logger.Error("SwaggerResolveError", err) + } else { + var marshalErr error + swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ") + if marshalErr != nil { + logger.Error("SwaggerMarshalError", err) + } + } + // End dereference + + // Replace incoming schemas with those we actually use in code + swaggerSchema = replaceIncomingSchemas(swaggerSchema) + } + return swaggerSchema +} + +func replaceIncomingSchemas(swaggerSchema []byte) []byte { + str := string(swaggerSchema) + + // Remember to include the double quotes in the replacement! + str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth()) + str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken()) + + str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority()) + str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority()) + + str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate()) + str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate("")) + + str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting()) + str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting()) + + str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser()) + str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser()) + + str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost()) + str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost()) + + str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream()) + str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream()) + + str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider()) + str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider()) + + return []byte(str) +} diff --git a/backend/internal/api/handler/settings.go b/backend/internal/api/handler/settings.go new file mode 100644 index 00000000..c73e7385 --- /dev/null +++ b/backend/internal/api/handler/settings.go @@ -0,0 +1,98 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/setting" + + "github.com/go-chi/chi" +) + +// GetSettings will return a list of Settings +// Route: GET /settings +func GetSettings() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, settings) + } + } +} + +// GetSetting will return a single Setting +// Route: GET /settings/{name} +func GetSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + sett, err := setting.GetByName(name) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, sett) + } + } +} + +// CreateSetting will create a Setting +// Route: POST /settings +func CreateSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newSetting setting.Model + err := json.Unmarshal(bodyBytes, &newSetting) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newSetting.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newSetting) + } +} + +// UpdateSetting ... +// Route: PUT /settings/{name} +func UpdateSetting() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + settingName := chi.URLParam(r, "name") + + setting, err := setting.GetByName(settingName) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &setting) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = setting.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, setting) + } + } +} diff --git a/backend/internal/api/handler/streams.go b/backend/internal/api/handler/streams.go new file mode 100644 index 00000000..04b277b6 --- /dev/null +++ b/backend/internal/api/handler/streams.go @@ -0,0 +1,129 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/stream" +) + +// GetStreams will return a list of Streams +// Route: GET /hosts/streams +func GetStreams() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, hosts) + } + } +} + +// GetStream will return a single Streams +// Route: GET /hosts/streams/{hostID} +func GetStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// CreateStream will create a Stream +// Route: POST /hosts/steams +func CreateStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newHost stream.Model + err := json.Unmarshal(bodyBytes, &newHost) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newHost.UserID = userID + + if err = newHost.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newHost) + } +} + +// UpdateStream ... +// Route: PUT /hosts/streams/{hostID} +func UpdateStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &host) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = host.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, host) + } + } +} + +// DeleteStream ... +// Route: DELETE /hosts/streams/{hostID} +func DeleteStream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID int + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, host.Delete()) + } + } +} diff --git a/backend/internal/api/handler/tokens.go b/backend/internal/api/handler/tokens.go new file mode 100644 index 00000000..8851e97e --- /dev/null +++ b/backend/internal/api/handler/tokens.go @@ -0,0 +1,77 @@ +package handler + +import ( + "encoding/json" + "net/http" + h "npm/internal/api/http" + + c "npm/internal/api/context" + "npm/internal/entity/auth" + "npm/internal/entity/user" + njwt "npm/internal/jwt" +) + +// tokenPayload is the structure we expect from a incoming login request +type tokenPayload struct { + Type string `json:"type"` + Identity string `json:"identity"` + Secret string `json:"secret"` +} + +// NewToken Also known as a Login, requesting a new token with credentials +// Route: POST /tokens +func NewToken() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Read the bytes from the body + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var payload tokenPayload + err := json.Unmarshal(bodyBytes, &payload) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Find user + userObj, userErr := user.GetByEmail(payload.Identity) + if userErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil) + return + } + + // Get Auth + authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type) + if userErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, authErr.Error(), nil) + return + } + + // Verify Auth + validateErr := authObj.ValidateSecret(payload.Secret) + if validateErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, validateErr.Error(), nil) + return + } + + if response, err := njwt.Generate(&userObj); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } + } +} + +// RefreshToken an existing token by given them a new one with the same claims +// Route: GET /tokens +func RefreshToken() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // TODO: Use your own methods to verify an existing user is + // able to refresh their token and then give them a new one + userObj, _ := user.GetByEmail("jc@jc21.com") + if response, err := njwt.Generate(&userObj); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } + } +} diff --git a/backend/internal/api/handler/users.go b/backend/internal/api/handler/users.go new file mode 100644 index 00000000..d2988966 --- /dev/null +++ b/backend/internal/api/handler/users.go @@ -0,0 +1,206 @@ +package handler + +import ( + "encoding/json" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/config" + "npm/internal/entity/auth" + "npm/internal/entity/user" + "npm/internal/errors" + "npm/internal/logger" + + "github.com/go-chi/chi" +) + +// GetUsers ... +// Route: GET /users +func GetUsers() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, users) + } + } +} + +// GetUser ... +// Route: GET /users/{userID} +func GetUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + + userID, _, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + user, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, user) + } + } +} + +// UpdateUser ... +// Route: PUT /users/{userID} +func UpdateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + userID, self, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + user, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &user) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if user.IsDisabled && self { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil) + return + } + + if err = user.Save(); err != nil { + if err == errors.ErrDuplicateEmailUser { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, user) + } + } +} + +// DeleteUser ... +// Route: DELETE /users/{userID} +func DeleteUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var userID int + var err error + if userID, err = getURLParamInt(r, "userID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + myUserID, _ := r.Context().Value(c.UserIDCtxKey).(int) + if myUserID == userID { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil) + return + } + + user, err := user.GetByID(userID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, user.Delete()) + } + } +} + +// CreateUser ... +// Route: POST /users +func CreateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newUser user.Model + err := json.Unmarshal(bodyBytes, &newUser) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = newUser.Save(); err != nil { + if err == errors.ErrDuplicateEmailUser { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + // newUser has been saved, now save their auth + if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 { + newUser.Auth.UserID = newUser.ID + if newUser.Auth.Type == auth.TypePassword { + err = newUser.Auth.SetPassword(newUser.Auth.Secret) + if err != nil { + logger.Error("SetPasswordError", err) + } + } + + if err = newUser.Auth.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) + return + } + + newUser.Auth.Secret = "" + } + + if !config.IsSetup { + config.IsSetup = true + logger.Info("A new user was created, leaving Setup Mode") + } + + h.ResultResponseJSON(w, r, http.StatusOK, newUser) + } +} + +// DeleteUsers is only available in debug mode for cypress tests +// Route: DELETE /users +func DeleteUsers() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + err := user.DeleteAll() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + // also change setup to true + config.IsSetup = false + logger.Info("Users have been wiped, entering Setup Mode") + h.ResultResponseJSON(w, r, http.StatusOK, true) + } + } +} + +func getUserIDFromRequest(r *http.Request) (int, bool, error) { + userIDstr := chi.URLParam(r, "userID") + + var userID int + self := false + if userIDstr == "me" { + // Get user id from Token + userID, _ = r.Context().Value(c.UserIDCtxKey).(int) + self = true + } else { + var userIDerr error + if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil { + return 0, false, userIDerr + } + } + return userID, self, nil +} diff --git a/backend/internal/api/http/requests.go b/backend/internal/api/http/requests.go new file mode 100644 index 00000000..c07d3fa6 --- /dev/null +++ b/backend/internal/api/http/requests.go @@ -0,0 +1,46 @@ +package http + +import ( + "context" + "encoding/json" + "errors" + + "github.com/qri-io/jsonschema" +) + +var ( + // ErrInvalidJSON ... + ErrInvalidJSON = errors.New("JSON is invalid") + // ErrInvalidPayload ... + ErrInvalidPayload = errors.New("Payload is invalid") +) + +// ValidateRequestSchema takes a Schema and the Content to validate against it +func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) { + var jsonErrors []jsonschema.KeyError + var schemaBytes = []byte(schema) + + // Make sure the body is valid JSON + if !isJSON(requestBody) { + return jsonErrors, ErrInvalidJSON + } + + rs := &jsonschema.Schema{} + if err := json.Unmarshal(schemaBytes, rs); err != nil { + return jsonErrors, err + } + + var validationErr error + ctx := context.TODO() + if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 { + return jsonErrors, validationErr + } + + // Valid + return nil, nil +} + +func isJSON(bytes []byte) bool { + var js map[string]interface{} + return json.Unmarshal(bytes, &js) == nil +} diff --git a/backend/internal/api/http/responses.go b/backend/internal/api/http/responses.go new file mode 100644 index 00000000..fa2d9767 --- /dev/null +++ b/backend/internal/api/http/responses.go @@ -0,0 +1,90 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + c "npm/internal/api/context" + "npm/internal/logger" + + "github.com/qri-io/jsonschema" +) + +// Response interface for standard API results +type Response struct { + Result interface{} `json:"result"` + Error interface{} `json:"error,omitempty"` +} + +// ErrorResponse interface for errors returned via the API +type ErrorResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` + Invalid interface{} `json:"invalid,omitempty"` +} + +// ResultResponseJSON will write the result as json to the http output +func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + + var response Response + resultClass := fmt.Sprintf("%v", reflect.TypeOf(result)) + + if resultClass == "http.ErrorResponse" { + response = Response{ + Error: result, + } + } else { + response = Response{ + Result: result, + } + } + + var payload []byte + var err error + if getPrettyPrintFromContext(r) { + payload, err = json.MarshalIndent(response, "", " ") + } else { + payload, err = json.Marshal(response) + } + + if err != nil { + logger.Error("ResponseMarshalError", err) + } + + fmt.Fprint(w, string(payload)) +} + +// ResultSchemaErrorJSON will format the result as a standard error object and send it for output +func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errors []jsonschema.KeyError) { + errorResponse := ErrorResponse{ + Code: http.StatusBadRequest, + Message: "Request failed validation", + Invalid: errors, + } + + ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse) +} + +// ResultErrorJSON will format the result as a standard error object and send it for output +func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) { + errorResponse := ErrorResponse{ + Code: status, + Message: message, + Invalid: extended, + } + + ResultResponseJSON(w, r, status, errorResponse) +} + +// getPrettyPrintFromContext returns the PrettyPrint setting +func getPrettyPrintFromContext(r *http.Request) bool { + pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool) + if !ok { + return false + } + return pretty +} diff --git a/backend/internal/api/middleware/access_control.go b/backend/internal/api/middleware/access_control.go new file mode 100644 index 00000000..18bca31b --- /dev/null +++ b/backend/internal/api/middleware/access_control.go @@ -0,0 +1,13 @@ +package middleware + +import ( + "net/http" +) + +// AccessControl sets http headers for responses +func AccessControl(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 00000000..925c7992 --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,64 @@ +package middleware + +import ( + "context" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/config" + "npm/internal/entity/user" + njwt "npm/internal/jwt" + "npm/internal/logger" + + "github.com/go-chi/jwtauth" +) + +// DecodeAuth ... +func DecodeAuth() func(http.Handler) http.Handler { + privateKey, privateKeyParseErr := njwt.GetPrivateKey() + if privateKeyParseErr != nil && privateKey == nil { + logger.Error("PrivateKeyParseError", privateKeyParseErr) + } + + publicKey, publicKeyParseErr := njwt.GetPublicKey() + if publicKeyParseErr != nil && publicKey == nil { + logger.Error("PublicKeyParseError", publicKeyParseErr) + } + + tokenAuth := jwtauth.New("RS256", privateKey, publicKey) + return jwtauth.Verifier(tokenAuth) +} + +// Enforce is a authentication middleware to enforce access from the +// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised +// response for any unverified tokens and passes the good ones through. +func Enforce() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if config.IsSetup { + token, claims, err := jwtauth.FromContext(ctx) + + if err != nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil) + return + } + + userID := int(claims["uid"].(float64)) + _, enabled := user.IsEnabled(userID) + if token == nil || !token.Valid || !enabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + // Add claims to context + ctx = context.WithValue(ctx, c.UserIDCtxKey, userID) + } + + // Token is authenticated, continue as normal + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/backend/internal/api/middleware/body_context.go b/backend/internal/api/middleware/body_context.go new file mode 100644 index 00000000..68cfaa08 --- /dev/null +++ b/backend/internal/api/middleware/body_context.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "context" + "io/ioutil" + "net/http" + + c "npm/internal/api/context" +) + +// BodyContext simply adds the body data to a context item +func BodyContext() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Grab the Body Data + var body []byte + if r.Body != nil { + body, _ = ioutil.ReadAll(r.Body) + } + // Add it to the context + ctx := r.Context() + ctx = context.WithValue(ctx, c.BodyCtxKey, body) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/backend/internal/api/middleware/cors.go b/backend/internal/api/middleware/cors.go new file mode 100644 index 00000000..a3b2fbb9 --- /dev/null +++ b/backend/internal/api/middleware/cors.go @@ -0,0 +1,88 @@ +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi" +) + +var methodMap = []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodTrace, +} + +func getRouteMethods(routes chi.Router, path string) []string { + var methods []string + tctx := chi.NewRouteContext() + for _, method := range methodMap { + if routes.Match(tctx, method, path) { + methods = append(methods, method) + } + } + return methods +} + +var headersAllowedByCORS = []string{ + "Authorization", + "Host", + "Content-Type", + "Connection", + "User-Agent", + "Cache-Control", + "Accept-Encoding", + "X-Jumbo-AppKey", + "X-Jumbo-SKey", + "X-Jumbo-SV", + "X-Jumbo-Timestamp", + "X-Jumbo-Version", + "X-Jumbo-Customer-Id", +} + +// Cors ... +func Cors(routes chi.Router) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + methods := getRouteMethods(routes, r.URL.Path) + if len(methods) == 0 { + // no route no cors + next.ServeHTTP(w, r) + return + } + methods = append(methods, http.MethodOptions) + w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) + w.Header().Set("Access-Control-Allow-Headers", + strings.Join(headersAllowedByCORS, ","), + ) + next.ServeHTTP(w, r) + }) + } +} + +// Options ... +func Options(routes chi.Router) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + methods := getRouteMethods(routes, r.URL.Path) + if len(methods) == 0 { + // no route shouldn't have options + next.ServeHTTP(w, r) + return + } + if r.Method == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, "{}") + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/enforce_setup.go b/backend/internal/api/middleware/enforce_setup.go new file mode 100644 index 00000000..3c75ccd9 --- /dev/null +++ b/backend/internal/api/middleware/enforce_setup.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "fmt" + "net/http" + + h "npm/internal/api/http" + "npm/internal/config" +) + +// EnforceSetup will error if the config setup doesn't match what is required +func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if config.IsSetup != shouldBeSetup { + state := "during" + if config.IsSetup { + state = "after" + } + h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/filters.go b/backend/internal/api/middleware/filters.go new file mode 100644 index 00000000..8a3f84cf --- /dev/null +++ b/backend/internal/api/middleware/filters.go @@ -0,0 +1,114 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/model" + "npm/internal/util" + "strings" + + "github.com/qri-io/jsonschema" +) + +// Filters will accept a pre-defined schemaData to validate against the GET query params +// passed in to this endpoint. This will ensure that the filters are not injecting SQL. +// After we have determined what the Filters are to be, they are saved on the Context +// to be used later in other endpoints. +func Filters(schemaData string) func(http.Handler) http.Handler { + reservedFilterKeys := []string{ + "limit", + "offset", + "sort", + "order", + "t", // This is used as a timestamp paramater in some clients and can be ignored + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var filters []model.Filter + for key, val := range r.URL.Query() { + key = strings.ToLower(key) + + // Split out the modifier from the field name and set a default modifier + var keyParts []string + keyParts = strings.Split(key, ":") + if len(keyParts) == 1 { + // Default modifier + keyParts = append(keyParts, "equals") + } + + // Only use this filter if it's not a reserved get param + if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) { + for _, valItem := range val { + // Check that the val isn't empty + if len(strings.TrimSpace(valItem)) > 0 { + valSlice := []string{valItem} + if keyParts[1] == "in" || keyParts[1] == "notin" { + valSlice = strings.Split(valItem, ",") + } + + filters = append(filters, model.Filter{ + Field: keyParts[0], + Modifier: keyParts[1], + Value: valSlice, + }) + } + } + } + } + + // Only validate schema if there are filters to validate + if len(filters) > 0 { + ctx := r.Context() + + // Marshal the Filters in to a JSON string so that the Schema Validation works against it + filterData, marshalErr := json.MarshalIndent(filters, "", " ") + if marshalErr != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil) + return + } + + // Create root schema + rs := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(schemaData), rs); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil) + return + } + + // Validate it + errors, jsonError := rs.ValidateBytes(ctx, filterData) + if jsonError != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil) + return + } + + if len(errors) > 0 { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors) + return + } + + ctx = context.WithValue(ctx, c.FiltersCtxKey, filters) + next.ServeHTTP(w, r.WithContext(ctx)) + + } else { + next.ServeHTTP(w, r) + } + }) + } +} + +// GetFiltersFromContext returns the Filters +func GetFiltersFromContext(r *http.Request) []model.Filter { + filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter) + if !ok { + // the assertion failed + var emptyFilters []model.Filter + return emptyFilters + } + return filters +} diff --git a/backend/internal/api/middleware/pretty_print.go b/backend/internal/api/middleware/pretty_print.go new file mode 100644 index 00000000..270d2a24 --- /dev/null +++ b/backend/internal/api/middleware/pretty_print.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "context" + "net/http" + + c "npm/internal/api/context" +) + +// PrettyPrint will determine whether the request should be pretty printed in output +// with ?pretty=1 or ?pretty=true +func PrettyPrint(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + prettyStr := r.URL.Query().Get("pretty") + if prettyStr == "1" || prettyStr == "true" { + ctx := r.Context() + ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + next.ServeHTTP(w, r) + } + }) +} diff --git a/backend/internal/api/middleware/schema.go b/backend/internal/api/middleware/schema.go new file mode 100644 index 00000000..d3dba570 --- /dev/null +++ b/backend/internal/api/middleware/schema.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + + "github.com/qri-io/jsonschema" +) + +// CheckRequestSchema ... +func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) { + // Create root schema + rs := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(schemaData), rs); err != nil { + return nil, fmt.Errorf("Schema Fatal: %v", err) + } + + // Validate it + schemaErrors, jsonError := rs.ValidateBytes(ctx, payload) + if jsonError != nil { + return nil, jsonError + } + + return schemaErrors, nil +} + +// EnforceRequestSchema accepts a schema and validates the request body against it +func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Get content from context + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil) + return + } + + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go new file mode 100644 index 00000000..944f993b --- /dev/null +++ b/backend/internal/api/router.go @@ -0,0 +1,171 @@ +package api + +import ( + "net/http" + "time" + + "npm/internal/api/handler" + "npm/internal/api/middleware" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/entity/certificate" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/entity/host" + "npm/internal/entity/setting" + "npm/internal/entity/stream" + "npm/internal/entity/user" + "npm/internal/logger" + + "github.com/go-chi/chi" + chiMiddleware "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" +) + +// NewRouter returns a new router object +func NewRouter() http.Handler { + // Cors + cors := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"}, + AllowCredentials: true, + MaxAge: 300, + }) + + r := chi.NewRouter() + r.Use( + middleware.AccessControl, + middleware.Cors(r), + middleware.Options(r), + cors.Handler, + chiMiddleware.RealIP, + chiMiddleware.Recoverer, + chiMiddleware.Throttle(5), + chiMiddleware.Timeout(30*time.Second), + middleware.PrettyPrint, + middleware.DecodeAuth(), + middleware.BodyContext(), + ) + + return applyRoutes(r) +} + +// applyRoutes is where the magic happens +func applyRoutes(r chi.Router) chi.Router { + r.NotFound(handler.NotFound()) + r.MethodNotAllowed(handler.NotAllowed()) + + // API + r.Route("/api", func(r chi.Router) { + r.Get("/", handler.Health()) + r.Get("/schema", handler.Schema()) + r.With(middleware.EnforceSetup(true), middleware.Enforce()). + Get("/config", handler.Config()) + + // Tokens + r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) { + r.With(middleware.EnforceRequestSchema(schema.GetToken())). + Post("/", handler.NewToken()) + r.With(middleware.Enforce()). + Get("/", handler.RefreshToken()) + }) + + // Users + r.With(middleware.Enforce()).Route("/users", func(r chi.Router) { + r.With(middleware.EnforceSetup(true)).Get("/{userID:(?:[0-9]+|me)}", handler.GetUser()) + r.With(middleware.EnforceSetup(true)).Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser()) + r.With(middleware.EnforceSetup(true)).With(middleware.Filters(user.GetFilterSchema())). + Get("/", handler.GetUsers()) + r.With(middleware.EnforceRequestSchema(schema.CreateUser())). + Post("/", handler.CreateUser()) + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser())). + Put("/{userID:(?:[0-9]+|me)}", handler.UpdateUser()) + + // Auth + r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth())). + Post("/{userID:(?:[0-9]+|me)}/auth", handler.SetAuth()) + }) + + // Only available in debug mode: delete users without auth + if config.GetLogLevel() == logger.DebugLevel { + r.Delete("/users", handler.DeleteUsers()) + } + + // Settings + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/settings", func(r chi.Router) { + r.With(middleware.Filters(setting.GetFilterSchema())). + Get("/", handler.GetSettings()) + r.Get("/{name}", handler.GetSetting()) + r.With(middleware.EnforceRequestSchema(schema.CreateSetting())). + Post("/", handler.CreateSetting()) + r.With(middleware.EnforceRequestSchema(schema.UpdateSetting())). + Put("/{name}", handler.UpdateSetting()) + }) + + // DNS Providers + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/dns-providers", func(r chi.Router) { + r.With(middleware.Filters(dnsprovider.GetFilterSchema())). + Get("/", handler.GetDNSProviders()) + r.Get("/{providerID:[0-9]+}", handler.GetDNSProvider()) + r.Delete("/{providerID:[0-9]+}", handler.DeleteDNSProvider()) + r.With(middleware.EnforceRequestSchema(schema.CreateDNSProvider())). + Post("/", handler.CreateDNSProvider()) + r.With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())). + Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider()) + }) + + // Certificate Authorities + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/certificate-authorities", func(r chi.Router) { + r.With(middleware.Filters(certificateauthority.GetFilterSchema())). + Get("/", handler.GetCertificateAuthorities()) + r.Get("/{caID:[0-9]+}", handler.GetCertificateAuthority()) + r.Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority()) + r.With(middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())). + Post("/", handler.CreateCertificateAuthority()) + r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). + Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority()) + }) + + // Certificates + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/certificates", func(r chi.Router) { + r.With(middleware.Filters(certificate.GetFilterSchema())). + Get("/", handler.GetCertificates()) + r.Get("/{certificateID:[0-9]+}", handler.GetCertificate()) + r.Delete("/{certificateID:[0-9]+}", handler.DeleteCertificate()) + r.With(middleware.EnforceRequestSchema(schema.CreateCertificate())). + Post("/", handler.CreateCertificate()) + /* + r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())). + Put("/{certificateID:[0-9]+}", handler.UpdateCertificate()) + */ + r.Put("/{certificateID:[0-9]+}", handler.UpdateCertificate()) + }) + + // Hosts + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/hosts", func(r chi.Router) { + r.With(middleware.Filters(host.GetFilterSchema())). + Get("/", handler.GetHosts()) + r.Get("/{hostID:[0-9]+}", handler.GetHost()) + r.Delete("/{hostID:[0-9]+}", handler.DeleteHost()) + r.With(middleware.EnforceRequestSchema(schema.CreateHost())). + Post("/", handler.CreateHost()) + r.With(middleware.EnforceRequestSchema(schema.UpdateHost())). + Put("/{hostID:[0-9]+}", handler.UpdateHost()) + }) + + // Streams + r.With(middleware.EnforceSetup(true), middleware.Enforce()).Route("/streams", func(r chi.Router) { + r.With(middleware.Filters(stream.GetFilterSchema())). + Get("/", handler.GetStreams()) + r.Get("/{hostID:[0-9]+}", handler.GetStream()) + r.Delete("/{hostID:[0-9]+}", handler.DeleteStream()) + r.With(middleware.EnforceRequestSchema(schema.CreateStream())). + Post("/", handler.CreateStream()) + r.With(middleware.EnforceRequestSchema(schema.UpdateStream())). + Put("/{hostID:[0-9]+}", handler.UpdateStream()) + }) + }) + + return r +} diff --git a/backend/internal/api/router_test.go b/backend/internal/api/router_test.go new file mode 100644 index 00000000..78ec784f --- /dev/null +++ b/backend/internal/api/router_test.go @@ -0,0 +1,44 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "npm/internal/config" + + "github.com/stretchr/testify/assert" +) + +var ( + r = NewRouter() + version = "3.0.0" + commit = "abcdefgh" + sentryDSN = "" +) + +// Tear up/down +func TestMain(m *testing.M) { + config.Init(&version, &commit, &sentryDSN) + code := m.Run() + os.Exit(code) +} + +func TestGetHealthz(t *testing.T) { + respRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/", nil) + + r.ServeHTTP(respRec, req) + assert.Equal(t, http.StatusOK, respRec.Code) + assert.Contains(t, respRec.Body.String(), "healthy") +} + +func TestNonExistent(t *testing.T) { + respRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil) + + r.ServeHTTP(respRec, req) + assert.Equal(t, http.StatusNotFound, respRec.Code) + assert.Equal(t, respRec.Body.String(), `{"result":null,"error":{"code":404,"message":"Not found"}}`, "404 Message should match") +} diff --git a/backend/internal/api/schema/certificates.go b/backend/internal/api/schema/certificates.go new file mode 100644 index 00000000..5014b636 --- /dev/null +++ b/backend/internal/api/schema/certificates.go @@ -0,0 +1,191 @@ +package schema + +import ( + "fmt" + + "npm/internal/entity/certificate" +) + +// This validation is strictly for Custom certificates +// and the combination of values that must be defined +func createCertificateCustom() string { + return fmt.Sprintf(` + { + "type": "object", + "required": [ + "type", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("custom"), stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for HTTP certificates +// and the combination of values that must be defined +func createCertificateHTTP() string { + return fmt.Sprintf(` + { + "type": "object", + "required": [ + "type", + "certificate_authority_id", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "certificate_authority_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("http"), intMinOne, stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for DNS certificates +// and the combination of values that must be defined +func createCertificateDNS() string { + return fmt.Sprintf(` + { + "type": "object", + "required": [ + "type", + "certificate_authority_id", + "dns_provider_id", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "certificate_authority_id": %s, + "dns_provider_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("dns"), intMinOne, intMinOne, stringMinMax(1, 100), domainNames()) +} + +// This validation is strictly for MKCERT certificates +// and the combination of values that must be defined +func createCertificateMkcert() string { + return fmt.Sprintf(` + { + "type": "object", + "required": [ + "type", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, strictString("mkcert"), stringMinMax(1, 100), domainNames()) +} + +func updateCertificateHTTP() string { + return fmt.Sprintf(` + { + "type": "object", + "minProperties": 1, + "properties": { + "certificate_authority_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, intMinOne, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateDNS() string { + return fmt.Sprintf(` + { + "type": "object", + "minProperties": 1, + "properties": { + "certificate_authority_id": %s, + "dns_provider_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, intMinOne, intMinOne, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateCustom() string { + return fmt.Sprintf(` + { + "type": "object", + "minProperties": 1, + "properties": { + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateMkcert() string { + return fmt.Sprintf(` + { + "type": "object", + "minProperties": 1, + "properties": { + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, stringMinMax(1, 100), domainNames()) +} + +// CreateCertificate is the schema for incoming data validation +func CreateCertificate() string { + return fmt.Sprintf(` + { + "oneOf": [%s, %s, %s, %s] + }`, createCertificateHTTP(), createCertificateDNS(), createCertificateCustom(), createCertificateMkcert()) +} + +// UpdateCertificate is the schema for incoming data validation +func UpdateCertificate(certificateType string) string { + switch certificateType { + case certificate.TypeHTTP: + return updateCertificateHTTP() + case certificate.TypeDNS: + return updateCertificateDNS() + case certificate.TypeCustom: + return updateCertificateCustom() + case certificate.TypeMkcert: + return updateCertificateMkcert() + default: + return fmt.Sprintf(` + { + "oneOf": [%s, %s, %s, %s] + }`, updateCertificateHTTP(), updateCertificateDNS(), updateCertificateCustom(), updateCertificateMkcert()) + } +} diff --git a/backend/internal/api/schema/common.go b/backend/internal/api/schema/common.go new file mode 100644 index 00000000..dda1b9be --- /dev/null +++ b/backend/internal/api/schema/common.go @@ -0,0 +1,61 @@ +package schema + +import "fmt" + +func strictString(value string) string { + return fmt.Sprintf(`{ + "type": "string", + "pattern": "^%s$" + }`, value) +} + +const intMinOne = ` +{ + "type": "integer", + "minimum": 1 +} +` + +func stringMinMax(minLength, maxLength int) string { + return fmt.Sprintf(`{ + "type": "string", + "minLength": %d, + "maxLength": %d + }`, minLength, maxLength) +} + +func userRoles() string { + return fmt.Sprintf(` + { + "type": "array", + "items": %s + }`, stringMinMax(2, 50)) +} + +func domainNames() string { + return fmt.Sprintf(` + { + "type": "array", + "minItems": 1, + "items": %s + }`, stringMinMax(4, 255)) +} + +const anyType = ` +{ + "anyOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "integer" + } + ] +} +` diff --git a/backend/internal/api/schema/create_certificate_authority.go b/backend/internal/api/schema/create_certificate_authority.go new file mode 100644 index 00000000..a4b69bb3 --- /dev/null +++ b/backend/internal/api/schema/create_certificate_authority.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// CreateCertificateAuthority is the schema for incoming data validation +func CreateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "acme2_url" + ], + "properties": { + "name": %s, + "acme2_url": %s + } + } + `, stringMinMax(1, 100), stringMinMax(8, 255)) +} diff --git a/backend/internal/api/schema/create_dns_provider.go b/backend/internal/api/schema/create_dns_provider.go new file mode 100644 index 00000000..6da73e72 --- /dev/null +++ b/backend/internal/api/schema/create_dns_provider.go @@ -0,0 +1,25 @@ +package schema + +import "fmt" + +// CreateDNSProvider is the schema for incoming data validation +func CreateDNSProvider() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "provider_key", + "name", + "meta" + ], + "properties": { + "provider_key": %s, + "name": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100)) +} diff --git a/backend/internal/api/schema/create_host.go b/backend/internal/api/schema/create_host.go new file mode 100644 index 00000000..c87d393b --- /dev/null +++ b/backend/internal/api/schema/create_host.go @@ -0,0 +1,75 @@ +package schema + +import "fmt" + +// CreateHost is the schema for incoming data validation +// This schema supports 3 possible types with different data combinations: +// - proxy +// - redirection +// - dead +func CreateHost() string { + return fmt.Sprintf(` + { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "domain_names" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^proxy$" + }, + "listen_interface": %s, + "domain_names": %s, + "upstream_id": { + "type": "integer" + }, + "certificate_id": { + "type": "integer" + }, + "access_list_id": { + "type": "integer" + }, + "ssl_forced": { + "type": "boolean" + }, + "caching_enabled": { + "type": "boolean" + }, + "block_exploits": { + "type": "boolean" + }, + "allow_websocket_upgrade": { + "type": "boolean" + }, + "http2_support": { + "type": "boolean" + }, + "hsts_enabled": { + "type": "boolean" + }, + "hsts_subdomains": { + "type": "boolean" + }, + "paths": { + "type": "string" + }, + "upstream_options": { + "type": "string" + }, + "advanced_config": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + } + } + } + ] + } + `, stringMinMax(0, 255), domainNames()) +} diff --git a/backend/internal/api/schema/create_setting.go b/backend/internal/api/schema/create_setting.go new file mode 100644 index 00000000..dca3869c --- /dev/null +++ b/backend/internal/api/schema/create_setting.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// CreateSetting is the schema for incoming data validation +func CreateSetting() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": %s, + "value": %s + } + } + `, stringMinMax(2, 100), anyType) +} diff --git a/backend/internal/api/schema/create_stream.go b/backend/internal/api/schema/create_stream.go new file mode 100644 index 00000000..792b8818 --- /dev/null +++ b/backend/internal/api/schema/create_stream.go @@ -0,0 +1,27 @@ +package schema + +import "fmt" + +// CreateStream is the schema for incoming data validation +func CreateStream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "name", + "domain_names" + ], + "properties": { + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/create_user.go b/backend/internal/api/schema/create_user.go new file mode 100644 index 00000000..b46a8621 --- /dev/null +++ b/backend/internal/api/schema/create_user.go @@ -0,0 +1,42 @@ +package schema + +import "fmt" + +// CreateUser is the schema for incoming data validation +func CreateUser() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "email", + "roles", + "is_disabled" + ], + "properties": { + "name": %s, + "nickname": %s, + "email": %s, + "roles": %s, + "is_disabled": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": [ + "type", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^password$" + }, + "secret": %s + } + } + } + } + `, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), userRoles(), stringMinMax(8, 255)) +} diff --git a/backend/internal/api/schema/get_token.go b/backend/internal/api/schema/get_token.go new file mode 100644 index 00000000..fe1a9502 --- /dev/null +++ b/backend/internal/api/schema/get_token.go @@ -0,0 +1,28 @@ +package schema + +import "fmt" + +// GetToken is the schema for incoming data validation +// nolint: gosec +func GetToken() string { + stdField := stringMinMax(1, 255) + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "identity", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^password$" + }, + "identity": %s, + "secret": %s + } + } + `, stdField, stdField) +} diff --git a/backend/internal/api/schema/set_auth.go b/backend/internal/api/schema/set_auth.go new file mode 100644 index 00000000..5e6ea975 --- /dev/null +++ b/backend/internal/api/schema/set_auth.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// SetAuth is the schema for incoming data validation +func SetAuth() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": %s, + "value": %s + } + } + `, stringMinMax(2, 100), anyType) +} diff --git a/backend/internal/api/schema/update_certificate_authority.go b/backend/internal/api/schema/update_certificate_authority.go new file mode 100644 index 00000000..9bdeadc5 --- /dev/null +++ b/backend/internal/api/schema/update_certificate_authority.go @@ -0,0 +1,17 @@ +package schema + +import "fmt" + +// UpdateCertificateAuthority is the schema for incoming data validation +func UpdateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": %s, + "acme2_url": %s + } + } + `, stringMinMax(1, 100), stringMinMax(8, 255)) +} diff --git a/backend/internal/api/schema/update_dns_provider.go b/backend/internal/api/schema/update_dns_provider.go new file mode 100644 index 00000000..10d77145 --- /dev/null +++ b/backend/internal/api/schema/update_dns_provider.go @@ -0,0 +1,19 @@ +package schema + +import "fmt" + +// UpdateDNSProvider is the schema for incoming data validation +func UpdateDNSProvider() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(1, 100)) +} diff --git a/backend/internal/api/schema/update_host.go b/backend/internal/api/schema/update_host.go new file mode 100644 index 00000000..33e411c5 --- /dev/null +++ b/backend/internal/api/schema/update_host.go @@ -0,0 +1,22 @@ +package schema + +import "fmt" + +// UpdateHost is the schema for incoming data validation +func UpdateHost() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/update_setting.go b/backend/internal/api/schema/update_setting.go new file mode 100644 index 00000000..98686935 --- /dev/null +++ b/backend/internal/api/schema/update_setting.go @@ -0,0 +1,16 @@ +package schema + +import "fmt" + +// UpdateSetting is the schema for incoming data validation +func UpdateSetting() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "value": %s + } + } + `, anyType) +} diff --git a/backend/internal/api/schema/update_stream.go b/backend/internal/api/schema/update_stream.go new file mode 100644 index 00000000..8b2763a6 --- /dev/null +++ b/backend/internal/api/schema/update_stream.go @@ -0,0 +1,22 @@ +package schema + +import "fmt" + +// UpdateStream is the schema for incoming data validation +func UpdateStream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": %s, + "name": %s, + "domain_names": %s, + "expires_on": %s, + "meta": { + "type": "object" + } + } + } + `, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne) +} diff --git a/backend/internal/api/schema/update_user.go b/backend/internal/api/schema/update_user.go new file mode 100644 index 00000000..cf17bd7d --- /dev/null +++ b/backend/internal/api/schema/update_user.go @@ -0,0 +1,22 @@ +package schema + +import "fmt" + +// UpdateUser is the schema for incoming data validation +func UpdateUser() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": %s, + "nickname": %s, + "email": %s, + "roles": %s, + "is_disabled": { + "type": "boolean" + } + } + } + `, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), userRoles()) +} diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go new file mode 100644 index 00000000..c64de44a --- /dev/null +++ b/backend/internal/api/server.go @@ -0,0 +1,19 @@ +package api + +import ( + "fmt" + "net/http" + + "npm/internal/logger" +) + +const httpPort = 3000 + +// StartServer creates a http server +func StartServer() { + logger.Info("Server starting on port %v", httpPort) + err := http.ListenAndServe(fmt.Sprintf(":%v", httpPort), NewRouter()) + if err != nil { + logger.Error("HttpListenError", err) + } +} diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js deleted file mode 100644 index 422b4f46..00000000 --- a/backend/internal/audit-log.js +++ /dev/null @@ -1,78 +0,0 @@ -const error = require('../lib/error'); -const auditLogModel = require('../models/audit-log'); - -const internalAuditLog = { - - /** - * All logs - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('auditlog:list') - .then(() => { - let query = auditLogModel - .query() - .orderBy('created_on', 'DESC') - .orderBy('id', 'DESC') - .limit(100) - .allowEager('[user]'); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('meta', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * This method should not be publicly used, it doesn't check certain things. It will be assumed - * that permission to add to audit log is already considered, however the access token is used for - * default user id determination. - * - * @param {Access} access - * @param {Object} data - * @param {String} data.action - * @param {Number} [data.user_id] - * @param {Number} [data.object_id] - * @param {Number} [data.object_type] - * @param {Object} [data.meta] - * @returns {Promise} - */ - add: (access, data) => { - return new Promise((resolve, reject) => { - // Default the user id - if (typeof data.user_id === 'undefined' || !data.user_id) { - data.user_id = access.token.getUserId(1); - } - - if (typeof data.action === 'undefined' || !data.action) { - reject(new error.InternalValidationError('Audit log entry must contain an Action')); - } else { - // Make sure at least 1 of the IDs are set and action - resolve(auditLogModel - .query() - .insert({ - user_id: data.user_id, - action: data.action, - object_type: data.object_type || '', - object_id: data.object_id || 0, - meta: data.meta || {} - })); - } - }); - } -}; - -module.exports = internalAuditLog; diff --git a/backend/internal/cache/cache.go b/backend/internal/cache/cache.go new file mode 100644 index 00000000..347ce92f --- /dev/null +++ b/backend/internal/cache/cache.go @@ -0,0 +1,51 @@ +package cache + +import ( + "time" + + "npm/internal/entity/setting" + "npm/internal/logger" +) + +// Cache is a memory cache +type Cache struct { + Settings *map[string]setting.Model +} + +// Status is the status of last update +type Status struct { + LastUpdate time.Time + Valid bool +} + +// NewCache will create and return a new Cache object +func NewCache() *Cache { + return &Cache{ + Settings: nil, + } +} + +// Refresh will refresh all cache items +func (c *Cache) Refresh() { + c.RefreshSettings() +} + +// Clear will clear the cache +func (c *Cache) Clear() { + c.Settings = nil +} + +// RefreshSettings will refresh the settings from db +func (c *Cache) RefreshSettings() { + logger.Info("Cache refreshing Settings") + /* + c.ProductOffers = client.GetProductOffers() + + if c.ProductOffers != nil { + c.Status["product_offers"] = Status{ + LastUpdate: time.Now(), + Valid: true, + } + } + */ +} diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js deleted file mode 100644 index c887e681..00000000 --- a/backend/internal/certificate.js +++ /dev/null @@ -1,1073 +0,0 @@ -const fs = require('fs'); -const _ = require('lodash'); -const logger = require('../logger').ssl; -const error = require('../lib/error'); -const certificateModel = require('../models/certificate'); -const internalAuditLog = require('./audit-log'); -const tempWrite = require('temp-write'); -const utils = require('../lib/utils'); -const moment = require('moment'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; -const le_staging = process.env.NODE_ENV !== 'production'; -const internalNginx = require('./nginx'); -const internalHost = require('./host'); -const certbot_command = '/opt/certbot/bin/certbot'; -const le_config = '/etc/letsencrypt.ini'; -const dns_plugins = require('../global/certbot-dns-plugins'); - -function omissions() { - return ['is_deleted']; -} - -const internalCertificate = { - - allowed_ssl_files: ['certificate', 'certificate_key', 'intermediate_certificate'], - interval_timeout: 1000 * 60 * 60, // 1 hour - interval: null, - interval_processing: false, - - initTimer: () => { - logger.info('Let\'s Encrypt Renewal Timer initialized'); - internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.interval_timeout); - // And do this now as well - internalCertificate.processExpiringHosts(); - }, - - /** - * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required - */ - processExpiringHosts: () => { - if (!internalCertificate.interval_processing) { - internalCertificate.interval_processing = true; - logger.info('Renewing SSL certs close to expiry...'); - - let cmd = certbot_command + ' renew --non-interactive --quiet ' + - '--config "' + le_config + '" ' + - '--preferred-challenges "dns,http" ' + - '--disable-hook-validation ' + - (le_staging ? '--staging' : ''); - - return utils.exec(cmd) - .then((result) => { - if (result) { - logger.info('Renew Result: ' + result); - } - - return internalNginx.reload() - .then(() => { - logger.info('Renew Complete'); - return result; - }); - }) - .then(() => { - // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times - return certificateModel - .query() - .where('is_deleted', 0) - .andWhere('provider', 'letsencrypt') - .then((certificates) => { - if (certificates && certificates.length) { - let promises = []; - - certificates.map(function (certificate) { - promises.push( - internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .where('id', certificate.id) - .andWhere('provider', 'letsencrypt') - .patch({ - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }); - }) - .catch((err) => { - // Don't want to stop the train here, just log the error - logger.error(err.message); - }) - ); - }); - - return Promise.all(promises); - } - }); - }) - .then(() => { - internalCertificate.interval_processing = false; - }) - .catch((err) => { - logger.error(err); - internalCertificate.interval_processing = false; - }); - } - }, - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - return access.can('certificates:create', data) - .then(() => { - data.owner_user_id = access.token.getUserId(1); - - if (data.provider === 'letsencrypt') { - data.nice_name = data.domain_names.sort().join(', '); - } - - return certificateModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - // Request a new Cert from LE. Let the fun begin. - - // 1. Find out any hosts that are using any of the hostnames in this cert - // 2. Disable them in nginx temporarily - // 3. Generate the LE config - // 4. Request cert - // 5. Remove LE config - // 6. Re-instate previously disabled hosts - - // 1. Find out any hosts that are using any of the hostnames in this cert - return internalHost.getHostsWithDomains(certificate.domain_names) - .then((in_use_result) => { - // 2. Disable them in nginx temporarily - return internalCertificate.disableInUseHosts(in_use_result) - .then(() => { - return in_use_result; - }); - }) - .then((in_use_result) => { - // With DNS challenge no config is needed, so skip 3 and 5. - if (certificate.meta.dns_challenge) { - return internalNginx.reload().then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); - }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalCertificate.enableInUseHosts(in_use_result) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); - } else { - // 3. Generate the LE config - return internalNginx.generateLetsEncryptRequestConfig(certificate) - .then(internalNginx.reload) - .then(() => { - // 4. Request cert - return internalCertificate.requestLetsEncryptSsl(certificate); - }) - .then(() => { - // 5. Remove LE config - return internalNginx.deleteLetsEncryptRequestConfig(certificate); - }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalNginx.deleteLetsEncryptRequestConfig(certificate) - .then(() => { - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); - } - }) - .then(() => { - // At this point, the letsencrypt cert should exist on disk. - // Lets get the expiry date from the file and update the row silently - return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') - .then((cert_info) => { - return certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }) - .then((saved_row) => { - // Add cert data for audit log - saved_row.meta = _.assign({}, saved_row.meta, { - letsencrypt_certificate: cert_info - }); - - return saved_row; - }); - }); - }).catch(async (error) => { - // Delete the certificate from the database if it was not created successfully - await certificateModel - .query() - .deleteById(certificate.id); - - throw error; - }); - } else { - return certificate; - } - }).then((certificate) => { - - data.meta = _.assign({}, data.meta || {}, certificate.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'certificate', - object_id: certificate.id, - meta: data - }) - .then(() => { - return certificate; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.email] - * @param {String} [data.name] - * @return {Promise} - */ - update: (access, data) => { - return access.can('certificates:update', data.id) - .then((/*access_data*/) => { - return internalCertificate.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Certificate could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - return certificateModel - .query() - .omit(omissions()) - .patchAndFetchById(row.id, data) - .then((saved_row) => { - saved_row.meta = internalCertificate.cleanMeta(saved_row.meta); - data.meta = internalCertificate.cleanMeta(data.meta); - - // Add row.nice_name for custom certs - if (saved_row.provider === 'other') { - data.nice_name = saved_row.nice_name; - } - - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'certificate', - object_id: row.id, - meta: _.omit(data, ['expires_on']) // this prevents json circular reference because expires_on might be raw - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('certificates:get', data.id) - .then((access_data) => { - let query = certificateModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('certificates:delete', data.id) - .then(() => { - return internalCertificate.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return certificateModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Add to audit log - row.meta = internalCertificate.cleanMeta(row.meta); - - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'certificate', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }) - .then(() => { - if (row.provider === 'letsencrypt') { - // Revoke the cert - return internalCertificate.revokeLetsEncryptSsl(row); - } - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Certs - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('certificates:list') - .then((access_data) => { - let query = certificateModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner]') - .orderBy('nice_name', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = certificateModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - }, - - /** - * @param {Object} certificate - * @returns {Promise} - */ - writeCustomCert: (certificate) => { - if (debug_mode) { - logger.info('Writing Custom Certificate:', certificate); - } - - let dir = '/data/custom_ssl/npm-' + certificate.id; - - return new Promise((resolve, reject) => { - if (certificate.provider === 'letsencrypt') { - reject(new Error('Refusing to write letsencrypt certs here')); - return; - } - - let cert_data = certificate.meta.certificate; - if (typeof certificate.meta.intermediate_certificate !== 'undefined') { - cert_data = cert_data + '\n' + certificate.meta.intermediate_certificate; - } - - try { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - } catch (err) { - reject(err); - return; - } - - fs.writeFile(dir + '/fullchain.pem', cert_data, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }) - .then(() => { - return new Promise((resolve, reject) => { - fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Array} data.domain_names - * @param {String} data.meta.letsencrypt_email - * @param {Boolean} data.meta.letsencrypt_agree - * @returns {Promise} - */ - createQuickCertificate: (access, data) => { - return internalCertificate.create(access, { - provider: 'letsencrypt', - domain_names: data.domain_names, - meta: data.meta - }); - }, - - /** - * Validates that the certs provided are good. - * No access required here, nothing is changed or stored. - * - * @param {Object} data - * @param {Object} data.files - * @returns {Promise} - */ - validate: (data) => { - return new Promise((resolve) => { - // Put file contents into an object - let files = {}; - _.map(data.files, (file, name) => { - if (internalCertificate.allowed_ssl_files.indexOf(name) !== -1) { - files[name] = file.data.toString(); - } - }); - - resolve(files); - }) - .then((files) => { - // For each file, create a temp file and write the contents to it - // Then test it depending on the file type - let promises = []; - _.map(files, (content, type) => { - promises.push(new Promise((resolve) => { - if (type === 'certificate_key') { - resolve(internalCertificate.checkPrivateKey(content)); - } else { - // this should handle `certificate` and intermediate certificate - resolve(internalCertificate.getCertificateInfo(content, true)); - } - }).then((res) => { - return {[type]: res}; - })); - }); - - return Promise.all(promises) - .then((files) => { - let data = {}; - - _.each(files, (file) => { - data = _.assign({}, data, file); - }); - - return data; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Object} data.files - * @returns {Promise} - */ - upload: (access, data) => { - return internalCertificate.get(access, {id: data.id}) - .then((row) => { - if (row.provider !== 'other') { - throw new error.ValidationError('Cannot upload certificates for this type of provider'); - } - - return internalCertificate.validate(data) - .then((validations) => { - if (typeof validations.certificate === 'undefined') { - throw new error.ValidationError('Certificate file was not provided'); - } - - _.map(data.files, (file, name) => { - if (internalCertificate.allowed_ssl_files.indexOf(name) !== -1) { - row.meta[name] = file.data.toString(); - } - }); - - // TODO: This uses a mysql only raw function that won't translate to postgres - return internalCertificate.update(access, { - id: data.id, - expires_on: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'), - domain_names: [validations.certificate.cn], - meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later - }) - .then((certificate) => { - console.log('ROWMETA:', row.meta); - certificate.meta = row.meta; - return internalCertificate.writeCustomCert(certificate); - }); - }) - .then(() => { - return _.pick(row.meta, internalCertificate.allowed_ssl_files); - }); - }); - }, - - /** - * Uses the openssl command to validate the private key. - * It will save the file to disk first, then run commands on it, then delete the file. - * - * @param {String} private_key This is the entire key contents as a string - */ - checkPrivateKey: (private_key) => { - return tempWrite(private_key, '/tmp') - .then((filepath) => { - return new Promise((resolve, reject) => { - const failTimeout = setTimeout(() => { - reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.')); - }, 10000); - utils - .exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ') - .then((result) => { - clearTimeout(failTimeout); - if (!result.toLowerCase().includes('key is valid')) { - reject(new error.ValidationError('Result Validation Error: ' + result)); - } - fs.unlinkSync(filepath); - resolve(true); - }) - .catch((err) => { - clearTimeout(failTimeout); - fs.unlinkSync(filepath); - reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err)); - }); - }); - }); - }, - - /** - * Uses the openssl command to both validate and get info out of the certificate. - * It will save the file to disk first, then run commands on it, then delete the file. - * - * @param {String} certificate This is the entire cert contents as a string - * @param {Boolean} [throw_expired] Throw when the certificate is out of date - */ - getCertificateInfo: (certificate, throw_expired) => { - return tempWrite(certificate, '/tmp') - .then((filepath) => { - return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired) - .then((cert_data) => { - fs.unlinkSync(filepath); - return cert_data; - }).catch((err) => { - fs.unlinkSync(filepath); - throw err; - }); - }); - }, - - /** - * Uses the openssl command to both validate and get info out of the certificate. - * It will save the file to disk first, then run commands on it, then delete the file. - * - * @param {String} certificate_file The file location on disk - * @param {Boolean} [throw_expired] Throw when the certificate is out of date - */ - getCertificateInfoFromFile: (certificate_file, throw_expired) => { - let cert_data = {}; - - return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') - .then((result) => { - // subject=CN = something.example.com - let regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; - let match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine subject from certificate: ' + result); - } - - cert_data['cn'] = match[1]; - }) - .then(() => { - return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); - }) - .then((result) => { - // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 - let regex = /^(?:issuer=)?(.*)$/gim; - let match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine issuer from certificate: ' + result); - } - - cert_data['issuer'] = match[1]; - }) - .then(() => { - return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); - }) - .then((result) => { - // notBefore=Jul 14 04:04:29 2018 GMT - // notAfter=Oct 12 04:04:29 2018 GMT - let valid_from = null; - let valid_to = null; - - let lines = result.split('\n'); - lines.map(function (str) { - let regex = /^(\S+)=(.*)$/gim; - let match = regex.exec(str.trim()); - - if (match && typeof match[2] !== 'undefined') { - let date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10); - - if (match[1].toLowerCase() === 'notbefore') { - valid_from = date; - } else if (match[1].toLowerCase() === 'notafter') { - valid_to = date; - } - } - }); - - if (!valid_from || !valid_to) { - throw new error.ValidationError('Could not determine dates from certificate: ' + result); - } - - if (throw_expired && valid_to < parseInt(moment().format('X'), 10)) { - throw new error.ValidationError('Certificate has expired'); - } - - cert_data['dates'] = { - from: valid_from, - to: valid_to - }; - - return cert_data; - }).catch((err) => { - throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err); - }); - }, - - /** - * Cleans the ssl keys from the meta object and sets them to "true" - * - * @param {Object} meta - * @param {Boolean} [remove] - * @returns {Object} - */ - cleanMeta: function (meta, remove) { - internalCertificate.allowed_ssl_files.map((key) => { - if (typeof meta[key] !== 'undefined' && meta[key]) { - if (remove) { - delete meta[key]; - } else { - meta[key] = true; - } - } - }); - - return meta; - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - requestLetsEncryptSsl: (certificate) => { - logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - let cmd = certbot_command + ' certonly --non-interactive ' + - '--config "' + le_config + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--preferred-challenges "dns,http" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - (le_staging ? '--staging' : ''); - - if (debug_mode) { - logger.info('Command:', cmd); - } - - return utils.exec(cmd) - .then((result) => { - logger.success(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`) - * @param {String | null} credentials the content of this providers credentials file - * @param {String} propagation_seconds the cloudflare api token - * @returns {Promise} - */ - requestLetsEncryptSslWithDnsChallenge: (certificate) => { - const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - - if (!dns_plugin) { - throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); - } - - logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - const credentials_cmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\''; - const prepare_cmd = 'pip install ' + dns_plugin.package_name + '==' + dns_plugin.package_version + ' ' + dns_plugin.dependencies; - - // Whether the plugin has a ---credentials argument - const has_config_arg = certificate.meta.dns_provider !== 'route53'; - - let main_cmd = - certbot_command + ' certonly --non-interactive ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - '--authenticator ' + dns_plugin.full_plugin_name + ' ' + - ( - has_config_arg - ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentials_loc + '"' - : '' - ) + - ( - certificate.meta.propagation_seconds !== undefined - ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds - : '' - ) + - (le_staging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd; - } - - if (debug_mode) { - logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd}`); - } - - return utils.exec(credentials_cmd) - .then(() => { - return utils.exec(prepare_cmd) - .then(() => { - return utils.exec(main_cmd) - .then(async (result) => { - logger.info(result); - return result; - }); - }); - }).catch(async (err) => { - // Don't fail if file does not exist - const delete_credentials_cmd = `rm -f '${credentials_loc}' || true`; - await utils.exec(delete_credentials_cmd); - throw err; - }); - }, - - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - renew: (access, data) => { - return access.can('certificates:update', data) - .then(() => { - return internalCertificate.get(access, data); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - let renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl; - - return renewMethod(certificate) - .then(() => { - return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); - }) - .then((cert_info) => { - return certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') - }); - }) - .then((updated_certificate) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'renewed', - object_type: 'certificate', - object_id: updated_certificate.id, - meta: updated_certificate - }) - .then(() => { - return updated_certificate; - }); - }); - } else { - throw new error.ValidationError('Only Let\'sEncrypt certificates can be renewed'); - } - }); - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - renewLetsEncryptSsl: (certificate) => { - logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - let cmd = certbot_command + ' renew --non-interactive ' + - '--config "' + le_config + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--preferred-challenges "dns,http" ' + - '--disable-hook-validation ' + - (le_staging ? '--staging' : ''); - - if (debug_mode) { - logger.info('Command:', cmd); - } - - return utils.exec(cmd) - .then((result) => { - logger.info(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - renewLetsEncryptSslWithDnsChallenge: (certificate) => { - const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - - if (!dns_plugin) { - throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); - } - - logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); - - let main_cmd = - certbot_command + ' renew --non-interactive ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--disable-hook-validation' + - (le_staging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd; - } - - if (debug_mode) { - logger.info('Command:', main_cmd); - } - - return utils.exec(main_cmd) - .then(async (result) => { - logger.info(result); - return result; - }); - }, - - /** - * @param {Object} certificate the certificate row - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - revokeLetsEncryptSsl: (certificate, throw_errors) => { - logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const main_cmd = certbot_command + ' revoke --non-interactive ' + - '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + - '--delete-after-revoke ' + - (le_staging ? '--staging' : ''); - - // Don't fail command if file does not exist - const delete_credentials_cmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; - - if (debug_mode) { - logger.info('Command:', main_cmd + '; ' + delete_credentials_cmd); - } - - return utils.exec(main_cmd) - .then(async (result) => { - await utils.exec(delete_credentials_cmd); - logger.info(result); - return result; - }) - .catch((err) => { - if (debug_mode) { - logger.error(err.message); - } - - if (throw_errors) { - throw err; - } - }); - }, - - /** - * @param {Object} certificate - * @returns {Boolean} - */ - hasLetsEncryptSslCerts: (certificate) => { - let le_path = '/etc/letsencrypt/live/npm-' + certificate.id; - - return fs.existsSync(le_path + '/fullchain.pem') && fs.existsSync(le_path + '/privkey.pem'); - }, - - /** - * @param {Object} in_use_result - * @param {Number} in_use_result.total_count - * @param {Array} in_use_result.proxy_hosts - * @param {Array} in_use_result.redirection_hosts - * @param {Array} in_use_result.dead_hosts - */ - disableInUseHosts: (in_use_result) => { - if (in_use_result.total_count) { - let promises = []; - - if (in_use_result.proxy_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts)); - } - - if (in_use_result.redirection_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('redirection_host', in_use_result.redirection_hosts)); - } - - if (in_use_result.dead_hosts.length) { - promises.push(internalNginx.bulkDeleteConfigs('dead_host', in_use_result.dead_hosts)); - } - - return Promise.all(promises); - - } else { - return Promise.resolve(); - } - }, - - /** - * @param {Object} in_use_result - * @param {Number} in_use_result.total_count - * @param {Array} in_use_result.proxy_hosts - * @param {Array} in_use_result.redirection_hosts - * @param {Array} in_use_result.dead_hosts - */ - enableInUseHosts: (in_use_result) => { - if (in_use_result.total_count) { - let promises = []; - - if (in_use_result.proxy_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts)); - } - - if (in_use_result.redirection_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('redirection_host', in_use_result.redirection_hosts)); - } - - if (in_use_result.dead_hosts.length) { - promises.push(internalNginx.bulkGenerateConfigs('dead_host', in_use_result.dead_hosts)); - } - - return Promise.all(promises); - - } else { - return Promise.resolve(); - } - } -}; - -module.exports = internalCertificate; diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 00000000..1eeb5ee8 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,78 @@ +package config + +import ( + "fmt" + golog "log" + "runtime" + + "npm/internal/logger" + + "github.com/getsentry/sentry-go" + "github.com/vrischmann/envconfig" +) + +// Init will parse environment variables into the Env struct +func Init(version, commit, sentryDSN *string) { + // ErrorReporting is enabled until we load the status of it from the DB later + ErrorReporting = true + + Version = *version + Commit = *commit + + if err := envconfig.Init(&Configuration); err != nil { + fmt.Printf("%+v\n", err) + } + + initLogger(*sentryDSN) + logger.Info("Build Version: %s (%s)", Version, Commit) + loadKeys() +} + +// Init initialises the Log object and return it +func initLogger(sentryDSN string) { + // this removes timestamp prefixes from logs + golog.SetFlags(0) + + switch Configuration.Log.Level { + case "debug": + logLevel = logger.DebugLevel + case "warn": + logLevel = logger.WarnLevel + case "error": + logLevel = logger.ErrorLevel + default: + logLevel = logger.InfoLevel + } + + err := logger.Configure(&logger.Config{ + LogThreshold: logLevel, + Formatter: Configuration.Log.Format, + SentryConfig: sentry.ClientOptions{ + // This is the jc21 NginxProxyManager Sentry project, + // errors will be reported here (if error reporting is enable) + // and this project is private. No personal information should + // be sent in any error messages, only stacktraces. + Dsn: sentryDSN, + Release: Commit, + Dist: Version, + Environment: fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), + }, + }) + + if err != nil { + logger.Error("LoggerConfigurationError", err) + } +} + +// GetLogLevel returns the logger const level +func GetLogLevel() logger.Level { + return logLevel +} + +func isError(errorClass string, err error) bool { + if err != nil { + logger.Error(errorClass, err) + return true + } + return false +} diff --git a/backend/internal/config/keys.go b/backend/internal/config/keys.go new file mode 100644 index 00000000..9ac3e903 --- /dev/null +++ b/backend/internal/config/keys.go @@ -0,0 +1,112 @@ +package config + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + + "npm/internal/logger" +) + +var keysFolder string +var publicKeyFile string +var privateKeyFile string + +func loadKeys() { + // check if keys folder exists in data folder + keysFolder = fmt.Sprintf("%s/keys", Configuration.DataFolder) + publicKeyFile = fmt.Sprintf("%s/public.key", keysFolder) + privateKeyFile = fmt.Sprintf("%s/private.key", keysFolder) + + if _, err := os.Stat(keysFolder); os.IsNotExist(err) { + // nolint:errcheck,gosec + os.Mkdir(keysFolder, 0700) + } + + // check if keys exist on disk + _, publicKeyErr := os.Stat(publicKeyFile) + _, privateKeyErr := os.Stat(privateKeyFile) + + // generate keys if either one doesn't exist + if os.IsNotExist(publicKeyErr) || os.IsNotExist(privateKeyErr) { + generateKeys() + saveKeys() + } + + // Load keys from disk + // nolint:gosec + publicKeyBytes, publicKeyBytesErr := ioutil.ReadFile(publicKeyFile) + // nolint:gosec + privateKeyBytes, privateKeyBytesErr := ioutil.ReadFile(privateKeyFile) + PublicKey = string(publicKeyBytes) + PrivateKey = string(privateKeyBytes) + + if isError("PublicKeyReadError", publicKeyBytesErr) || isError("PrivateKeyReadError", privateKeyBytesErr) || PublicKey == "" || PrivateKey == "" { + logger.Warn("There was an error loading keys, proceeding to generate new RSA keys") + generateKeys() + saveKeys() + } +} + +func generateKeys() { + reader := rand.Reader + bitSize := 4096 + + key, err := rsa.GenerateKey(reader, bitSize) + if isError("RSAGenerateError", err) { + return + } + + privateKey := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + privateKeyBuffer := new(bytes.Buffer) + err = pem.Encode(privateKeyBuffer, privateKey) + if isError("PrivatePEMEncodeError", err) { + return + } + + asn1Bytes, err2 := asn1.Marshal(key.PublicKey) + if isError("RSAMarshalError", err2) { + return + } + + publicKey := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: asn1Bytes, + } + + publicKeyBuffer := new(bytes.Buffer) + err = pem.Encode(publicKeyBuffer, publicKey) + if isError("PublicPEMEncodeError", err) { + return + } + + PublicKey = publicKeyBuffer.String() + PrivateKey = privateKeyBuffer.String() + logger.Info("Generated new RSA keys") +} + +func saveKeys() { + err := ioutil.WriteFile(publicKeyFile, []byte(PublicKey), 0600) + if err != nil { + logger.Error("PublicKeyWriteError", err) + } else { + logger.Info("Saved Public Key: %s", publicKeyFile) + } + + err = ioutil.WriteFile(privateKeyFile, []byte(PrivateKey), 0600) + if err != nil { + logger.Error("PrivateKeyWriteError", err) + } else { + logger.Info("Saved Private Key: %s", privateKeyFile) + } +} diff --git a/backend/internal/config/vars.go b/backend/internal/config/vars.go new file mode 100644 index 00000000..f856a940 --- /dev/null +++ b/backend/internal/config/vars.go @@ -0,0 +1,34 @@ +package config + +import "npm/internal/logger" + +// Version is the version set by ldflags +var Version string + +// Commit is the git commit set by ldflags +var Commit string + +// IsSetup defines whether we have an admin user or not +var IsSetup bool + +// ErrorReporting defines whether we will send errors to Sentry +var ErrorReporting bool + +// PublicKey ... +var PublicKey string + +// PrivateKey ... +var PrivateKey string + +var logLevel logger.Level + +type log struct { + Level string `json:"level" envconfig:"optional,default=info"` + Format string `json:"format" envconfig:"optional,default=nice"` +} + +// Configuration ... +var Configuration struct { + DataFolder string `json:"data_folder" envconfig:"optional,default=/data"` + Log log `json:"log"` +} diff --git a/backend/internal/database/helpers.go b/backend/internal/database/helpers.go new file mode 100644 index 00000000..7553af3b --- /dev/null +++ b/backend/internal/database/helpers.go @@ -0,0 +1,46 @@ +package database + +import ( + "fmt" + "strings" + + "npm/internal/errors" + "npm/internal/model" + "npm/internal/util" +) + +const ( + // DateFormat for DateFormat + DateFormat = "2006-01-02" + // DateTimeFormat for DateTimeFormat + DateTimeFormat = "2006-01-02T15:04:05" +) + +// GetByQuery returns a row given a query, populating the model given +func GetByQuery(model interface{}, query string, params []interface{}) error { + db := GetInstance() + if db != nil { + err := db.Get(model, query, params...) + return err + } + + return errors.ErrDatabaseUnavailable +} + +// BuildOrderBySQL takes a `Sort` slice and constructs a query fragment +func BuildOrderBySQL(columns []string, sort *[]model.Sort) (string, []model.Sort) { + var sortStrings []string + var newSort []model.Sort + for _, sortItem := range *sort { + if util.SliceContainsItem(columns, sortItem.Field) { + sortStrings = append(sortStrings, fmt.Sprintf("`%s` %s", sortItem.Field, sortItem.Direction)) + newSort = append(newSort, sortItem) + } + } + + if len(sortStrings) > 0 { + return fmt.Sprintf("ORDER BY %s", strings.Join(sortStrings, ", ")), newSort + } + + return "", newSort +} diff --git a/backend/internal/database/setup.go b/backend/internal/database/setup.go new file mode 100644 index 00000000..16f64559 --- /dev/null +++ b/backend/internal/database/setup.go @@ -0,0 +1,38 @@ +package database + +import ( + "database/sql" + + "npm/internal/config" + "npm/internal/errors" + "npm/internal/logger" +) + +// CheckSetup Quick check by counting the number of users in the database +func CheckSetup() { + query := `SELECT COUNT(*) FROM "user" WHERE is_deleted = $1 and is_disabled = $2` + db := GetInstance() + + if db != nil { + row := db.QueryRowx(query, false, false) + var totalRows int + queryErr := row.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("SetupError", queryErr) + return + } + if totalRows == 0 { + logger.Warn("No users found, starting in Setup Mode") + } else { + config.IsSetup = true + logger.Info("Application is setup") + } + + if config.ErrorReporting { + logger.Warn("Error reporting is enabled - Application Errors WILL be sent to Sentry, you can disable this in the Settings interface") + } + + } else { + logger.Error("DatabaseError", errors.ErrDatabaseUnavailable) + } +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go new file mode 100644 index 00000000..57528b49 --- /dev/null +++ b/backend/internal/database/sqlite.go @@ -0,0 +1,73 @@ +package database + +import ( + "fmt" + "os" + + "npm/internal/config" + "npm/internal/logger" + + "github.com/jmoiron/sqlx" + + // Blank import for Sqlite + _ "github.com/mattn/go-sqlite3" +) + +var dbInstance *sqlx.DB + +// NewDB creates a new connection +func NewDB() { + logger.Info("Creating new DB instance") + db := SqliteDB() + if db != nil { + dbInstance = db + } +} + +// GetInstance returns an existing or new instance +func GetInstance() *sqlx.DB { + if dbInstance == nil { + NewDB() + } else if err := dbInstance.Ping(); err != nil { + NewDB() + } + + return dbInstance +} + +// SqliteDB Create sqlite client +func SqliteDB() *sqlx.DB { + dbFile := fmt.Sprintf("%s/nginxproxymanager.db", config.Configuration.DataFolder) + autocreate(dbFile) + db, err := sqlx.Open("sqlite3", dbFile) + if err != nil { + logger.Error("SqliteError", err) + return nil + } + + return db +} + +// Commit will close and reopen the db file +func Commit() *sqlx.DB { + if dbInstance != nil { + err := dbInstance.Close() + if err != nil { + logger.Error("DatabaseCloseError", err) + } + } + NewDB() + return dbInstance +} + +func autocreate(dbFile string) { + if _, err := os.Stat(dbFile); os.IsNotExist(err) { + // Create it + logger.Info("Creating Sqlite DB: %s", dbFile) + _, err = os.Create(dbFile) + if err != nil { + logger.Error("FileCreateError", err) + } + Commit() + } +} diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js deleted file mode 100644 index d35fec25..00000000 --- a/backend/internal/dead-host.js +++ /dev/null @@ -1,461 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const deadHostModel = require('../models/dead_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalDeadHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('dead_hosts:create', data) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return deadHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalDeadHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // re-fetch with cert - return internalDeadHost.get(access, { - id: row.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row) - .then(() => { - return row; - }); - }) - .then((row) => { - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'dead-host', - object_id: row.id, - meta: data - }) - .then(() => { - return row; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @return {Promise} - */ - update: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('dead_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return deadHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'dead-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalDeadHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row) - .then((new_meta) => { - row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('dead_hosts:get', data.id) - .then((access_data) => { - let query = deadHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('dead_hosts:delete', data.id) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('dead_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'dead-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - enable: (access, data) => { - return access.can('dead_hosts:update', data.id) - .then(() => { - return internalDeadHost.get(access, { - id: data.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); - } - - row.enabled = 1; - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 1 - }) - .then(() => { - // Configure nginx - return internalNginx.configure(deadHostModel, 'dead_host', row); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'enabled', - object_type: 'dead-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - disable: (access, data) => { - return access.can('dead_hosts:update', data.id) - .then(() => { - return internalDeadHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); - } - - row.enabled = 0; - - return deadHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 0 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('dead_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'disabled', - object_type: 'dead-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Hosts - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('dead_hosts:list') - .then((access_data) => { - let query = deadHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,certificate]') - .orderBy('domain_names', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = deadHostModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - } -}; - -module.exports = internalDeadHost; diff --git a/backend/internal/entity/auth/methods.go b/backend/internal/entity/auth/methods.go new file mode 100644 index 00000000..9cfb7faa --- /dev/null +++ b/backend/internal/entity/auth/methods.go @@ -0,0 +1,82 @@ +package auth + +import ( + goerrors "errors" + "fmt" + + "npm/internal/database" +) + +// GetByID finds a auth by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByUserIDType finds a user by email +func GetByUserIDType(userID int, authType string) (Model, error) { + var m Model + err := m.LoadByUserIDType(userID, authType) + return m, err +} + +// Create will create a Auth from this model +func Create(auth *Model) (int, error) { + if auth.ID != 0 { + return 0, goerrors.New("Cannot create auth when model already has an ID") + } + + auth.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + secret, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :secret, + :is_deleted + )`, auth) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Auth from this model +func Update(auth *Model) error { + if auth.ID == 0 { + return goerrors.New("Cannot update auth when model doesn't have an ID") + } + + auth.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + type = :type, + secret = :secret, + is_deleted = :is_deleted + WHERE id = :id`, auth) + + return err +} diff --git a/backend/internal/entity/auth/model.go b/backend/internal/entity/auth/model.go new file mode 100644 index 00000000..b5640bbf --- /dev/null +++ b/backend/internal/entity/auth/model.go @@ -0,0 +1,98 @@ +package auth + +import ( + goerrors "errors" + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" + + "golang.org/x/crypto/bcrypt" +) + +const ( + tableName = "auth" + + // TypePassword is the Password Type + TypePassword = "password" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id"` + UserID int `json:"user_id" db:"user_id"` + Type string `json:"type" db:"type"` + Secret string `json:"secret,omitempty" db:"secret"` + CreatedOn types.DBDate `json:"created_on" db:"created_on"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", tableName) + params := []interface{}{id} + return m.getByQuery(query, params) +} + +// LoadByUserIDType will load from an ID +func (m *Model) LoadByUserIDType(userID int, authType string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE user_id = ? AND type = ? LIMIT 1", tableName) + params := []interface{}{userID, authType} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// SetPassword will generate a hashed password based on given string +func (m *Model) SetPassword(password string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost+2) + if err != nil { + return err + } + + m.Type = TypePassword + m.Secret = string(hash) + + return nil +} + +// ValidateSecret will check if a given secret matches the encrypted secret +func (m *Model) ValidateSecret(secret string) error { + if m.Type != TypePassword { + return goerrors.New("Could not validate Secret, auth type is not a Password") + } + + err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret)) + if err != nil { + return goerrors.New("Invalid Password") + } + + return nil +} diff --git a/backend/internal/entity/certificate/filters.go b/backend/internal/entity/certificate/filters.go new file mode 100644 index 00000000..5e321af8 --- /dev/null +++ b/backend/internal/entity/certificate/filters.go @@ -0,0 +1,25 @@ +package certificate + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/certificate/methods.go b/backend/internal/entity/certificate/methods.go new file mode 100644 index 00000000..27ae7d78 --- /dev/null +++ b/backend/internal/entity/certificate/methods.go @@ -0,0 +1,173 @@ +package certificate + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(certificate *Model) (int, error) { + if certificate.ID != 0 { + return 0, goerrors.New("Cannot create certificate when model already has an ID") + } + + certificate.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + certificate_authority_id, + dns_provider_id, + name, + domain_names, + expires_on, + status, + meta, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :certificate_authority_id, + :dns_provider_id, + :name, + :domain_names, + :expires_on, + :status, + :meta, + :is_deleted + )`, certificate) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Auth from this model +func Update(certificate *Model) error { + if certificate.ID == 0 { + return goerrors.New("Cannot update certificate when model doesn't have an ID") + } + + certificate.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + type = :type, + user_id = :user_id, + certificate_authority_id = :certificate_authority_id, + dns_provider_id = :dns_provider_id, + name = :name, + domain_names = :domain_names, + expires_on = :expires_on, + status = :status, + meta = :meta, + is_deleted = :is_deleted + WHERE id = :id`, certificate) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} + +// GetByStatus will select rows that are ready for requesting +func GetByStatus(status string) ([]Model, error) { + models := make([]Model, 0) + db := database.GetInstance() + + query := fmt.Sprintf(` + SELECT + t.* + FROM "%s" t + INNER JOIN "dns_provider" d ON d."id" = t."dns_provider_id" + INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id" + WHERE + t."type" IN ("http", "dns") AND + t."status" = ? AND + t."certificate_authority_id" > 0 AND + t."dns_provider_id" > 0 AND + t."is_deleted" = 0 + `, tableName) + + params := []interface{}{StatusReady} + err := db.Select(&models, query, params...) + if err != nil && err != sql.ErrNoRows { + logger.Error("GetByStatusError", err) + logger.Debug("Query: %s -- %+v", query, params) + } + + return models, err +} diff --git a/backend/internal/entity/certificate/model.go b/backend/internal/entity/certificate/model.go new file mode 100644 index 00000000..dc0dec5f --- /dev/null +++ b/backend/internal/entity/certificate/model.go @@ -0,0 +1,178 @@ +package certificate + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/types" +) + +const ( + tableName = "certificate" + + // TypeCustom ... + TypeCustom = "custom" + // TypeHTTP ... + TypeHTTP = "http" + // TypeDNS ... + TypeDNS = "dns" + // TypeMkcert ... + TypeMkcert = "mkcert" + // StatusReady is ready for certificate to be requested + StatusReady = "ready" + // StatusRequesting is process of being requested + StatusRequesting = "requesting" + // StatusFailed is a certicifate that failed to request + StatusFailed = "failed" + // StatusProvided is a certificate provided and ready for actual use + StatusProvided = "provided" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + ExpiresOn types.NullableDBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"` + Type string `json:"type" db:"type" filter:"type,string"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + CertificateAuthorityID int `json:"certificate_authority_id" db:"certificate_authority_id" filter:"certificate_authority_id,integer"` + DNSProviderID int `json:"dns_provider_id" db:"dns_provider_id" filter:"dns_provider_id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + Status string `json:"status" db:"status" filter:"status,string"` + ErrorMessage string `json:"error_message,omitempty" db:"error_message" filter:"error_message,string"` + Meta types.JSONB `json:"-" db:"meta"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions: + CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty"` + DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if !m.Validate() { + return fmt.Errorf("Certificate data is incorrect or incomplete for this type") + } + + m.setDefaultStatus() + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +// Validate will make sure the data given is expected. This object is a bit complicated, +// as there could be multiple combinations of values. +func (m *Model) Validate() bool { + switch m.Type { + case TypeCustom: + // TODO: make sure meta contains required fields + return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0 + + case TypeHTTP: + return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0 + + case TypeDNS: + return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0 + + case TypeMkcert: + return true + + default: + return false + } +} + +func (m *Model) setDefaultStatus() { + if m.ID == 0 { + // It's a new certificate + if m.Type == TypeCustom { + m.Status = StatusProvided + } else { + m.Status = StatusReady + } + } +} + +// Expand will populate attached objects for the model +func (m *Model) Expand() { + if m.CertificateAuthorityID > 0 { + certificateAuthority, _ := certificateauthority.GetByID(m.CertificateAuthorityID) + m.CertificateAuthority = &certificateAuthority + } + if m.DNSProviderID > 0 { + dnsProvider, _ := dnsprovider.GetByID(m.DNSProviderID) + m.DNSProvider = &dnsProvider + } +} + +// Request ... +func (m *Model) Request() error { + m.Expand() + m.Status = StatusRequesting + if err := m.Save(); err != nil { + return err + } + + // If error + m.Status = StatusFailed + m.ErrorMessage = "something" + if err := m.Save(); err != nil { + return err + } + + // If done + m.Status = StatusProvided + t := time.Now() + m.ExpiresOn.Time = &t + if err := m.Save(); err != nil { + return err + } + + return nil +} diff --git a/backend/internal/entity/certificate/structs.go b/backend/internal/entity/certificate/structs.go new file mode 100644 index 00000000..a9b99369 --- /dev/null +++ b/backend/internal/entity/certificate/structs.go @@ -0,0 +1,15 @@ +package certificate + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/certificateauthority/filters.go b/backend/internal/entity/certificateauthority/filters.go new file mode 100644 index 00000000..89d2d70f --- /dev/null +++ b/backend/internal/entity/certificateauthority/filters.go @@ -0,0 +1,25 @@ +package certificateauthority + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/certificateauthority/methods.go b/backend/internal/entity/certificateauthority/methods.go new file mode 100644 index 00000000..6a22b02f --- /dev/null +++ b/backend/internal/entity/certificateauthority/methods.go @@ -0,0 +1,125 @@ +package certificateauthority + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(ca *Model) (int, error) { + if ca.ID != 0 { + return 0, goerrors.New("Cannot create certificate authority when model already has an ID") + } + + ca.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + acme2_url, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :name, + :acme2_url, + :is_deleted + )`, ca) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a row from this model +func Update(ca *Model) error { + if ca.ID == 0 { + return goerrors.New("Cannot update certificate authority when model doesn't have an ID") + } + + ca.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + acme2_url = :acme2_url, + is_deleted = :is_deleted + WHERE id = :id`, ca) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("ListCertificateAuthoritiesError", queryErr) + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Error("ListCertificateAuthoritiesError", err) + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/certificateauthority/model.go b/backend/internal/entity/certificateauthority/model.go new file mode 100644 index 00000000..8a3ce47b --- /dev/null +++ b/backend/internal/entity/certificateauthority/model.go @@ -0,0 +1,67 @@ +package certificateauthority + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "certificate_authority" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Acme2URL string `json:"acme2_url" db:"acme2_url" filter:"acme2_url,string"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/certificateauthority/structs.go b/backend/internal/entity/certificateauthority/structs.go new file mode 100644 index 00000000..85e3521a --- /dev/null +++ b/backend/internal/entity/certificateauthority/structs.go @@ -0,0 +1,15 @@ +package certificateauthority + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/dnsprovider/filters.go b/backend/internal/entity/dnsprovider/filters.go new file mode 100644 index 00000000..683e8ec6 --- /dev/null +++ b/backend/internal/entity/dnsprovider/filters.go @@ -0,0 +1,25 @@ +package dnsprovider + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/dnsprovider/methods.go b/backend/internal/entity/dnsprovider/methods.go new file mode 100644 index 00000000..1eca9ab9 --- /dev/null +++ b/backend/internal/entity/dnsprovider/methods.go @@ -0,0 +1,131 @@ +package dnsprovider + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a row from this model +func Create(provider *Model) (int, error) { + if provider.ID != 0 { + return 0, goerrors.New("Cannot create dns provider when model already has an ID") + } + + provider.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + provider_key, + name, + meta, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :provider_key, + :name, + :meta, + :is_deleted + )`, provider) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a row from this model +func Update(provider *Model) error { + if provider.ID == 0 { + return goerrors.New("Cannot update dns provider when model doesn't have an ID") + } + + provider.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + provider_key = :provider_key, + name = :name, + meta = :meta, + is_deleted = :is_deleted + WHERE id = :id`, provider) + + return err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Error("ListDnsProvidersError", queryErr) + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Error("ListDnsProvidersError", err) + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/dnsprovider/model.go b/backend/internal/entity/dnsprovider/model.go new file mode 100644 index 00000000..70eb22cb --- /dev/null +++ b/backend/internal/entity/dnsprovider/model.go @@ -0,0 +1,73 @@ +package dnsprovider + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "dns_provider" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + ProviderKey string `json:"provider_key" db:"provider_key" filter:"provider_key,string"` + Name string `json:"name" db:"name" filter:"name,string"` + Meta types.JSONB `json:"meta" db:"meta"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a certificate as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/dnsprovider/structs.go b/backend/internal/entity/dnsprovider/structs.go new file mode 100644 index 00000000..835c947b --- /dev/null +++ b/backend/internal/entity/dnsprovider/structs.go @@ -0,0 +1,15 @@ +package dnsprovider + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for the list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/filters.go b/backend/internal/entity/filters.go new file mode 100644 index 00000000..55c5464f --- /dev/null +++ b/backend/internal/entity/filters.go @@ -0,0 +1,158 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" + + "npm/internal/model" +) + +// FilterMapFunction is a filter map function +type FilterMapFunction func(value []string) []string + +// FilterTagName ... +const FilterTagName = "filter" + +// DBTagName ... +const DBTagName = "db" + +// GenerateSQLFromFilters will return a Query and params for use as WHERE clause in SQL queries +// This will use a AND where clause approach. +func GenerateSQLFromFilters(filters []model.Filter, fieldMap map[string]string, fieldMapFunctions map[string]FilterMapFunction) (string, []interface{}) { + clauses := make([]string, 0) + params := make([]interface{}, 0) + + for _, filter := range filters { + // Lookup this filter field from the functions map + if _, ok := fieldMapFunctions[filter.Field]; ok { + filter.Value = fieldMapFunctions[filter.Field](filter.Value) + } + + // Lookup this filter field from the name map + if _, ok := fieldMap[filter.Field]; ok { + filter.Field = fieldMap[filter.Field] + } + + // Special case for LIKE queries, the column needs to be uppercase for comparison + fieldName := fmt.Sprintf("`%s`", filter.Field) + if strings.ToLower(filter.Modifier) == "contains" || strings.ToLower(filter.Modifier) == "starts" || strings.ToLower(filter.Modifier) == "ends" { + fieldName = fmt.Sprintf("UPPER(`%s`)", filter.Field) + } + + clauses = append(clauses, fmt.Sprintf("%s %s", fieldName, getSQLAssignmentFromModifier(filter, ¶ms))) + } + + return strings.Join(clauses, " AND "), params +} + +func getSQLAssignmentFromModifier(filter model.Filter, params *[]interface{}) string { + var clause string + + // Quick hacks + if filter.Modifier == "in" && len(filter.Value) == 1 { + filter.Modifier = "equals" + } else if filter.Modifier == "notin" && len(filter.Value) == 1 { + filter.Modifier = "not" + } + + switch strings.ToLower(filter.Modifier) { + default: + clause = "= ?" + case "not": + clause = "!= ?" + case "min": + clause = ">= ?" + case "max": + clause = "<= ?" + case "greater": + clause = "> ?" + case "lesser": + clause = "< ?" + + // LIKE modifiers: + case "contains": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE '%' || ? || '%'" + case "starts": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE ? || '%'" + case "ends": + *params = append(*params, strings.ToUpper(filter.Value[0])) + return "LIKE '%' || ?" + + // Array parameter modifiers: + case "in": + s, p := buildInArray(filter.Value) + *params = append(*params, p...) + return fmt.Sprintf("IN (%s)", s) + case "notin": + s, p := buildInArray(filter.Value) + *params = append(*params, p...) + return fmt.Sprintf("NOT IN (%s)", s) + } + + *params = append(*params, filter.Value[0]) + return clause +} + +// GetFilterMap ... +func GetFilterMap(m interface{}) map[string]string { + var filterMap = make(map[string]string) + + // TypeOf returns the reflection Type that represents the dynamic type of variable. + // If variable is a nil interface value, TypeOf returns nil. + t := reflect.TypeOf(m) + + // Iterate over all available fields and read the tag value + for i := 0; i < t.NumField(); i++ { + // Get the field, returns https://golang.org/pkg/reflect/#StructField + field := t.Field(i) + + // Get the field tag value + filterTag := field.Tag.Get(FilterTagName) + dbTag := field.Tag.Get(DBTagName) + if filterTag != "" && dbTag != "" && dbTag != "-" && filterTag != "-" { + // Filter tag can be a 2 part thing: name,type + // ie: account_id,integer + // So we need to split and use the first part + parts := strings.Split(filterTag, ",") + filterMap[parts[0]] = dbTag + filterMap[filterTag] = dbTag + } + } + + return filterMap +} + +// GetDBColumns ... +func GetDBColumns(m interface{}) []string { + var columns []string + t := reflect.TypeOf(m) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + dbTag := field.Tag.Get(DBTagName) + if dbTag != "" && dbTag != "-" { + columns = append(columns, dbTag) + } + } + + return columns +} + +func buildInArray(items []string) (string, []interface{}) { + // Query string placeholder + strs := make([]string, len(items)) + for i := 0; i < len(items); i++ { + strs[i] = "?" + } + + // Params as interface + params := make([]interface{}, len(items)) + for i, v := range items { + params[i] = v + } + + return strings.Join(strs, ", "), params +} diff --git a/backend/internal/entity/filters_schema.go b/backend/internal/entity/filters_schema.go new file mode 100644 index 00000000..9686293b --- /dev/null +++ b/backend/internal/entity/filters_schema.go @@ -0,0 +1,223 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" +) + +// GetFilterSchema creates a jsonschema for validating filters, based on the model +// object given and by reading the struct "filter" tags. +func GetFilterSchema(m interface{}) string { + var schemas []string + t := reflect.TypeOf(m) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + filterTag := field.Tag.Get(FilterTagName) + + if filterTag != "" && filterTag != "-" { + // split out tag value "field,filtreType" + // with a default filter type of string + items := strings.Split(filterTag, ",") + if len(items) == 1 { + items = append(items, "string") + } + + switch items[1] { + case "int": + fallthrough + case "integer": + schemas = append(schemas, intFieldSchema(items[0])) + case "bool": + fallthrough + case "boolean": + schemas = append(schemas, boolFieldSchema(items[0])) + case "date": + schemas = append(schemas, dateFieldSchema(items[0])) + case "regex": + if len(items) < 3 { + items = append(items, ".*") + } + schemas = append(schemas, regexFieldSchema(items[0], items[2])) + + default: + schemas = append(schemas, stringFieldSchema(items[0])) + } + } + } + + return newFilterSchema(schemas) +} + +// newFilterSchema is the main method to specify a new Filter Schema for use in Middleware +func newFilterSchema(fieldSchemas []string) string { + return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", ")) +} + +// boolFieldSchema returns the Field Schema for a Boolean accepted value field +func boolFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, boolModifiers, filterBool, filterBool) +} + +// intFieldSchema returns the Field Schema for a Integer accepted value field +func intFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +// stringFieldSchema returns the Field Schema for a String accepted value field +func stringFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + %s, + { + "type": "array", + "items": %s + } + ] + } + } + }`, fieldName, stringModifiers, filterString, filterString) +} + +// regexFieldSchema returns the Field Schema for a String accepted value field matching a Regex +func regexFieldSchema(fieldName string, regex string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "%s" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "%s" + } + } + ] + } + } + }`, fieldName, stringModifiers, regex, regex) +} + +// dateFieldSchema returns the Field Schema for a String accepted value field matching a Date format +func dateFieldSchema(fieldName string) string { + return fmt.Sprintf(`{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^%s$" + }, + "modifier": %s, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$" + } + } + ] + } + } + }`, fieldName, allModifiers) +} + +const allModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$" +}` + +const boolModifiers = `{ + "type": "string", + "pattern": "^(equals|not)$" +}` + +const stringModifiers = `{ + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin)$" +}` + +const filterBool = `{ + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" +}` + +const filterString = `{ + "type": "string", + "minLength": 1 +}` + +const baseFilterSchema = `{ + "type": "array", + "items": { + "oneOf": [ + %s + ] + } +}` diff --git a/backend/internal/entity/host/filters.go b/backend/internal/entity/host/filters.go new file mode 100644 index 00000000..643a417d --- /dev/null +++ b/backend/internal/entity/host/filters.go @@ -0,0 +1,25 @@ +package host + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/host/methods.go b/backend/internal/entity/host/methods.go new file mode 100644 index 00000000..1c6cf7ed --- /dev/null +++ b/backend/internal/entity/host/methods.go @@ -0,0 +1,171 @@ +package host + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Host by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a Host from this model +func Create(host *Model) (int, error) { + if host.ID != 0 { + return 0, goerrors.New("Cannot create host when model already has an ID") + } + + host.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + type, + listen_interface, + domain_names, + upstream_id, + certificate_id, + access_list_id, + ssl_forced, + caching_enabled, + block_exploits, + allow_websocket_upgrade, + http2_support, + hsts_enabled, + hsts_subdomains, + paths, + upstream_options, + advanced_config, + is_disabled, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :type, + :listen_interface, + :domain_names, + :upstream_id, + :certificate_id, + :access_list_id, + :ssl_forced, + :caching_enabled, + :block_exploits, + :allow_websocket_upgrade, + :http2_support, + :hsts_enabled, + :hsts_subdomains, + :paths, + :upstream_options, + :advanced_config, + :is_disabled, + :is_deleted + )`, host) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Host from this model +func Update(host *Model) error { + if host.ID == 0 { + return goerrors.New("Cannot update host when model doesn't have an ID") + } + + host.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + type = :type, + listen_interface = :listen_interface, + domain_names = :domain_names, + upstream_id = :upstream_id, + certificate_id = :certificate_id, + access_list_id = :access_list_id, + ssl_forced = :ssl_forced, + caching_enabled = :caching_enabled, + block_exploits = :block_exploits, + allow_websocket_upgrade = :allow_websocket_upgrade, + http2_support = :http2_support, + hsts_enabled = :hsts_enabled, + hsts_subdomains = :hsts_subdomains, + paths = :paths, + upstream_options = :upstream_options, + advanced_config = :advanced_config, + is_disabled = :is_disabled, + is_deleted = :is_deleted + WHERE id = :id`, host) + + return err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "domain_names", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/host/model.go b/backend/internal/entity/host/model.go new file mode 100644 index 00000000..8d2f851d --- /dev/null +++ b/backend/internal/entity/host/model.go @@ -0,0 +1,94 @@ +package host + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "host" + + // ProxyHostType ... + ProxyHostType = "proxy" + // RedirectionHostType ... + RedirectionHostType = "redirection" + // DeadHostType ... + DeadHostType = "dead" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Type string `json:"type" db:"type" filter:"type,string"` + ListenInterface string `json:"listen_interface" db:"listen_interface" filter:"listen_interface,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"` + CertificateID int `json:"certificate_id" db:"certificate_id" filter:"certificate_id,integer"` + AccessListID int `json:"access_list_id" db:"access_list_id" filter:"access_list_id,integer"` + SSLForced bool `json:"ssl_forced" db:"ssl_forced" filter:"ssl_forced,boolean"` + CachingEnabled bool `json:"caching_enabled" db:"caching_enabled" filter:"caching_enabled,boolean"` + BlockExploits bool `json:"block_exploits" db:"block_exploits" filter:"block_exploits,boolean"` + AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" db:"allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"` + HTTP2Support bool `json:"http2_support" db:"http2_support" filter:"http2_support,boolean"` + HSTSEnabled bool `json:"hsts_enabled" db:"hsts_enabled" filter:"hsts_enabled,boolean"` + HSTSSubdomains bool `json:"hsts_subdomains" db:"hsts_subdomains" filter:"hsts_subdomains,boolean"` + Paths string `json:"paths" db:"paths" filter:"paths,string"` + UpstreamOptions string `json:"upstream_options" db:"upstream_options" filter:"upstream_options,string"` + AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"` + IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a host as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/host/structs.go b/backend/internal/entity/host/structs.go new file mode 100644 index 00000000..dda3d6fe --- /dev/null +++ b/backend/internal/entity/host/structs.go @@ -0,0 +1,15 @@ +package host + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/lists_query.go b/backend/internal/entity/lists_query.go new file mode 100644 index 00000000..c277f675 --- /dev/null +++ b/backend/internal/entity/lists_query.go @@ -0,0 +1,80 @@ +package entity + +import ( + "fmt" + "reflect" + "strings" + + "npm/internal/database" + "npm/internal/model" +) + +// ListQueryBuilder should be able to return the query and params to get items agnostically based +// on given params. +func ListQueryBuilder(modelExample interface{}, tableName string, pageInfo *model.PageInfo, defaultSort model.Sort, filters []model.Filter, filterMapFunctions map[string]FilterMapFunction, returnCount bool) (string, []interface{}) { + var queryStrings []string + var whereStrings []string + var params []interface{} + + if returnCount { + queryStrings = append(queryStrings, "SELECT COUNT(*)") + } else { + queryStrings = append(queryStrings, "SELECT *") + } + + // nolint: gosec + queryStrings = append(queryStrings, fmt.Sprintf("FROM `%s`", tableName)) + + // Append filters to where clause: + if filters != nil { + filterMap := GetFilterMap(modelExample) + filterQuery, filterParams := GenerateSQLFromFilters(filters, filterMap, filterMapFunctions) + whereStrings = []string{filterQuery} + params = append(params, filterParams...) + } + + // Add is deletee check if model has the field + if hasDeletedField(modelExample) { + params = append(params, 0) + whereStrings = append(whereStrings, "`is_deleted` = ?") + } + + // Append where clauses to query + if len(whereStrings) > 0 { + // nolint: gosec + queryStrings = append(queryStrings, fmt.Sprintf("WHERE %s", strings.Join(whereStrings, " AND "))) + } + + if !returnCount { + var orderBy string + columns := GetDBColumns(modelExample) + orderBy, pageInfo.Sort = database.BuildOrderBySQL(columns, &pageInfo.Sort) + + if orderBy != "" { + queryStrings = append(queryStrings, orderBy) + } else { + pageInfo.Sort = append(pageInfo.Sort, defaultSort) + queryStrings = append(queryStrings, fmt.Sprintf("ORDER BY `%v` %v", defaultSort.Field, defaultSort.Direction)) + } + + params = append(params, pageInfo.Offset) + params = append(params, pageInfo.Limit) + queryStrings = append(queryStrings, "LIMIT ?, ?") + } + + return strings.Join(queryStrings, " "), params +} + +func hasDeletedField(modelExample interface{}) bool { + t := reflect.TypeOf(modelExample) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + dbTag := field.Tag.Get(DBTagName) + if dbTag == "is_deleted" { + return true + } + } + + return false +} diff --git a/backend/internal/entity/setting/apply.go b/backend/internal/entity/setting/apply.go new file mode 100644 index 00000000..72211a8e --- /dev/null +++ b/backend/internal/entity/setting/apply.go @@ -0,0 +1,15 @@ +package setting + +import ( + "npm/internal/config" + "npm/internal/logger" +) + +// ApplySettings will load settings from the DB and apply them where required +func ApplySettings() { + logger.Debug("Applying Settings") + + // Error-reporting + m, _ := GetByName("error-reporting") + config.ErrorReporting = m.Value.Decoded.(bool) +} diff --git a/backend/internal/entity/setting/filters.go b/backend/internal/entity/setting/filters.go new file mode 100644 index 00000000..0c4d6371 --- /dev/null +++ b/backend/internal/entity/setting/filters.go @@ -0,0 +1,25 @@ +package setting + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/setting/methods.go b/backend/internal/entity/setting/methods.go new file mode 100644 index 00000000..02211a03 --- /dev/null +++ b/backend/internal/entity/setting/methods.go @@ -0,0 +1,124 @@ +package setting + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/model" +) + +// GetByID finds a setting by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByName finds a setting by name +func GetByName(name string) (Model, error) { + var m Model + err := m.LoadByName(name) + return m, err +} + +// Create will Create a Setting from this model +func Create(setting *Model) (int, error) { + if setting.ID != 0 { + return 0, goerrors.New("Cannot create setting when model already has an ID") + } + + setting.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + value + ) VALUES ( + :created_on, + :modified_on, + :name, + :value + )`, setting) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Setting from this model +func Update(setting *Model) error { + if setting.ID == 0 { + return goerrors.New("Cannot update setting when model doesn't have an ID") + } + + setting.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + value = :value + WHERE id = :id`, setting) + + return err +} + +// List will return a list of settings +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/setting/model.go b/backend/internal/entity/setting/model.go new file mode 100644 index 00000000..303268ae --- /dev/null +++ b/backend/internal/entity/setting/model.go @@ -0,0 +1,69 @@ +package setting + +import ( + "fmt" + "strings" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "setting" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Value types.JSONB `json:"value" db:"value"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE `id` = ? LIMIT 1", tableName) + params := []interface{}{id} + return m.getByQuery(query, params) +} + +// LoadByName will load from a Name +func (m *Model) LoadByName(name string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE LOWER(`name`) = ? LIMIT 1", tableName) + params := []interface{}{strings.TrimSpace(strings.ToLower(name))} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + // Reapply settings + if err == nil { + ApplySettings() + } + + return err +} diff --git a/backend/internal/entity/setting/structs.go b/backend/internal/entity/setting/structs.go new file mode 100644 index 00000000..d585e9a1 --- /dev/null +++ b/backend/internal/entity/setting/structs.go @@ -0,0 +1,15 @@ +package setting + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/stream/filters.go b/backend/internal/entity/stream/filters.go new file mode 100644 index 00000000..f0078894 --- /dev/null +++ b/backend/internal/entity/stream/filters.go @@ -0,0 +1,25 @@ +package stream + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/stream/methods.go b/backend/internal/entity/stream/methods.go new file mode 100644 index 00000000..02ddd4d6 --- /dev/null +++ b/backend/internal/entity/stream/methods.go @@ -0,0 +1,135 @@ +package stream + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a auth by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// Create will create a Auth from this model +func Create(host *Model) (int, error) { + if host.ID != 0 { + return 0, goerrors.New("Cannot create stream when model already has an ID") + } + + host.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + provider, + name, + domain_names, + expires_on, + meta, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :provider, + :name, + :domain_names, + :expires_on, + :meta, + :is_deleted + )`, host) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a Host from this model +func Update(host *Model) error { + if host.ID == 0 { + return goerrors.New("Cannot update stream when model doesn't have an ID") + } + + host.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + provider = :provider, + name = :name, + domain_names = :domain_names, + expires_on = :expires_on, + meta = :meta, + is_deleted = :is_deleted + WHERE id = :id`, host) + + return err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/stream/model.go b/backend/internal/entity/stream/model.go new file mode 100644 index 00000000..60f47f75 --- /dev/null +++ b/backend/internal/entity/stream/model.go @@ -0,0 +1,75 @@ +package stream + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "stream" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + ExpiresOn types.DBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Provider string `json:"provider" db:"provider" filter:"provider,string"` + Name string `json:"name" db:"name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` + Meta types.JSONB `json:"-" db:"meta"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a host as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/stream/structs.go b/backend/internal/entity/stream/structs.go new file mode 100644 index 00000000..ec732c13 --- /dev/null +++ b/backend/internal/entity/stream/structs.go @@ -0,0 +1,15 @@ +package stream + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/user/filters.go b/backend/internal/entity/user/filters.go new file mode 100644 index 00000000..ea1c1640 --- /dev/null +++ b/backend/internal/entity/user/filters.go @@ -0,0 +1,25 @@ +package user + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema ... +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go new file mode 100644 index 00000000..211cadd0 --- /dev/null +++ b/backend/internal/entity/user/methods.go @@ -0,0 +1,181 @@ +package user + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a user by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByEmail finds a user by email +func GetByEmail(email string) (Model, error) { + var m Model + err := m.LoadByEmail(email) + return m, err +} + +// Create will create a User from given model +func Create(user *Model) (int, error) { + // We need to ensure that a user can't be created with the same email + // as an existing non-deleted user. Usually you would do this with the + // database schema, but it's a bit more complex because of the is_deleted field. + + if user.ID != 0 { + return 0, goerrors.New("Cannot create user when model already has an ID") + } + + // Check if an existing user with this email exists + _, err := GetByEmail(user.Email) + if err == nil { + return 0, errors.ErrDuplicateEmailUser + } + + user.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + name, + nickname, + email, + roles, + is_disabled + ) VALUES ( + :created_on, + :modified_on, + :name, + :nickname, + :email, + :roles, + :is_disabled + )`, user) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + return int(last), nil +} + +// Update will Update a User from this model +func Update(user *Model) error { + if user.ID == 0 { + return goerrors.New("Cannot update user when model doesn't have an ID") + } + + // Check that the email address isn't associated with another user + if existingUser, _ := GetByEmail(user.Email); existingUser.ID != 0 && existingUser.ID != user.ID { + return errors.ErrDuplicateEmailUser + } + + user.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + name = :name, + nickname = :nickname, + email = :email, + roles = :roles, + is_disabled = :is_disabled, + is_deleted = :is_deleted + WHERE id = :id`, user) + + return err +} + +// IsEnabled is used by middleware to ensure the user is still enabled +// returns (userExist, isEnabled) +func IsEnabled(userID int) (bool, bool) { + // nolint: gosec + query := `SELECT is_disabled FROM ` + fmt.Sprintf("`%s`", tableName) + ` WHERE id = ? AND is_deleted = ?` + disabled := true + db := database.GetInstance() + err := db.QueryRowx(query, userID, 0).Scan(&disabled) + + if err == sql.ErrNoRows { + return false, false + } else if err != nil { + logger.Error("QueryError", err) + } + + return true, !disabled +} + +// List will return a list of users +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + logger.Debug("Query: %s -- %+v", query, params) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + return result, queryErr + } + + // Get rows + var items []Model + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + logger.Debug("Query: %s -- %+v", query, params) + err := db.Select(&items, query, params...) + if err != nil { + return result, err + } + + for idx := range items { + items[idx].generateGravatar() + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} + +// DeleteAll will do just that, and should only be used for testing purposes. +func DeleteAll() error { + db := database.GetInstance() + _, err := db.Exec(fmt.Sprintf("DELETE FROM `%s`", tableName)) + return err +} diff --git a/backend/internal/entity/user/model.go b/backend/internal/entity/user/model.go new file mode 100644 index 00000000..0e605dd1 --- /dev/null +++ b/backend/internal/entity/user/model.go @@ -0,0 +1,97 @@ +package user + +import ( + "fmt" + "strings" + "time" + + "npm/internal/database" + "npm/internal/entity/auth" + "npm/internal/types" + + "github.com/drexedam/gravatar" +) + +const ( + tableName = "user" +) + +// Model is the user model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + Nickname string `json:"nickname" db:"nickname" filter:"nickname,string"` + Email string `json:"email" db:"email" filter:"email,email"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + Roles types.Roles `json:"roles" db:"roles"` + GravatarURL string `json:"gravatar_url"` + IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions + Auth *auth.Model `json:"auth,omitempty" db:"-"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + err := database.GetByQuery(m, query, params) + m.generateGravatar() + return err +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, false} + return m.getByQuery(query, params) +} + +// LoadByEmail will load from an Email +func (m *Model) LoadByEmail(email string) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE email = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{strings.TrimSpace(strings.ToLower(email)), false} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d + m.generateGravatar() +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + // Ensure email is nice + m.Email = strings.TrimSpace(strings.ToLower(m.Email)) + + if m.ID == 0 { + m.ID, err = Create(m) + } else { + err = Update(m) + } + + return err +} + +// Delete will mark a user as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} + +func (m *Model) generateGravatar() { + m.GravatarURL = gravatar.New(m.Email). + Size(128). + Default(gravatar.MysteryMan). + Rating(gravatar.Pg). + AvatarURL() +} diff --git a/backend/internal/entity/user/structs.go b/backend/internal/entity/user/structs.go new file mode 100644 index 00000000..f9f4490e --- /dev/null +++ b/backend/internal/entity/user/structs.go @@ -0,0 +1,15 @@ +package user + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/errors/errors.go b/backend/internal/errors/errors.go new file mode 100644 index 00000000..3336e64a --- /dev/null +++ b/backend/internal/errors/errors.go @@ -0,0 +1,10 @@ +package errors + +import "errors" + +// All error messages used by the service package to report +// problems back to calling clients +var ( + ErrDatabaseUnavailable = errors.New("Database is unavailable") + ErrDuplicateEmailUser = errors.New("A user already exists with this email address") +) diff --git a/backend/internal/host.js b/backend/internal/host.js deleted file mode 100644 index 58e1d09a..00000000 --- a/backend/internal/host.js +++ /dev/null @@ -1,235 +0,0 @@ -const _ = require('lodash'); -const proxyHostModel = require('../models/proxy_host'); -const redirectionHostModel = require('../models/redirection_host'); -const deadHostModel = require('../models/dead_host'); - -const internalHost = { - - /** - * Makes sure that the ssl_* and hsts_* fields play nicely together. - * ie: if there is no cert, then force_ssl is off. - * if force_ssl is off, then hsts_enabled is definitely off. - * - * @param {object} data - * @param {object} [existing_data] - * @returns {object} - */ - cleanSslHstsData: function (data, existing_data) { - existing_data = existing_data === undefined ? {} : existing_data; - - let combined_data = _.assign({}, existing_data, data); - - if (!combined_data.certificate_id) { - combined_data.ssl_forced = false; - combined_data.http2_support = false; - } - - if (!combined_data.ssl_forced) { - combined_data.hsts_enabled = false; - } - - if (!combined_data.hsts_enabled) { - combined_data.hsts_subdomains = false; - } - - return combined_data; - }, - - /** - * used by the getAll functions of hosts, this removes the certificate meta if present - * - * @param {Array} rows - * @returns {Array} - */ - cleanAllRowsCertificateMeta: function (rows) { - rows.map(function (row, idx) { - if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) { - rows[idx].certificate.meta = {}; - } - }); - - return rows; - }, - - /** - * used by the get/update functions of hosts, this removes the certificate meta if present - * - * @param {Object} row - * @returns {Object} - */ - cleanRowCertificateMeta: function (row) { - if (typeof row.certificate !== 'undefined' && row.certificate) { - row.certificate.meta = {}; - } - - return row; - }, - - /** - * This returns all the host types with any domain listed in the provided domain_names array. - * This is used by the certificates to temporarily disable any host that is using the domain - * - * @param {Array} domain_names - * @returns {Promise} - */ - getHostsWithDomains: function (domain_names) { - let promises = [ - proxyHostModel - .query() - .where('is_deleted', 0), - redirectionHostModel - .query() - .where('is_deleted', 0), - deadHostModel - .query() - .where('is_deleted', 0) - ]; - - return Promise.all(promises) - .then((promises_results) => { - let response_object = { - total_count: 0, - dead_hosts: [], - proxy_hosts: [], - redirection_hosts: [] - }; - - if (promises_results[0]) { - // Proxy Hosts - response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names); - response_object.total_count += response_object.proxy_hosts.length; - } - - if (promises_results[1]) { - // Redirection Hosts - response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names); - response_object.total_count += response_object.redirection_hosts.length; - } - - if (promises_results[2]) { - // Dead Hosts - response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names); - response_object.total_count += response_object.dead_hosts.length; - } - - return response_object; - }); - }, - - /** - * Internal use only, checks to see if the domain is already taken by any other record - * - * @param {String} hostname - * @param {String} [ignore_type] 'proxy', 'redirection', 'dead' - * @param {Integer} [ignore_id] Must be supplied if type was also supplied - * @returns {Promise} - */ - isHostnameTaken: function (hostname, ignore_type, ignore_id) { - let promises = [ - proxyHostModel - .query() - .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%'), - redirectionHostModel - .query() - .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%'), - deadHostModel - .query() - .where('is_deleted', 0) - .andWhere('domain_names', 'like', '%' + hostname + '%') - ]; - - return Promise.all(promises) - .then((promises_results) => { - let is_taken = false; - - if (promises_results[0]) { - // Proxy Hosts - if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) { - is_taken = true; - } - } - - if (promises_results[1]) { - // Redirection Hosts - if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) { - is_taken = true; - } - } - - if (promises_results[2]) { - // Dead Hosts - if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) { - is_taken = true; - } - } - - return { - hostname: hostname, - is_taken: is_taken - }; - }); - }, - - /** - * Private call only - * - * @param {String} hostname - * @param {Array} existing_rows - * @param {Integer} [ignore_id] - * @returns {Boolean} - */ - _checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) { - let is_taken = false; - - if (existing_rows && existing_rows.length) { - existing_rows.map(function (existing_row) { - existing_row.domain_names.map(function (existing_hostname) { - // Does this domain match? - if (existing_hostname.toLowerCase() === hostname.toLowerCase()) { - if (!ignore_id || ignore_id !== existing_row.id) { - is_taken = true; - } - } - }); - }); - } - - return is_taken; - }, - - /** - * Private call only - * - * @param {Array} hosts - * @param {Array} domain_names - * @returns {Array} - */ - _getHostsWithDomains: function (hosts, domain_names) { - let response = []; - - if (hosts && hosts.length) { - hosts.map(function (host) { - let host_matches = false; - - domain_names.map(function (domain_name) { - host.domain_names.map(function (host_domain_name) { - if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) { - host_matches = true; - } - }); - }); - - if (host_matches) { - response.push(host); - } - }); - } - - return response; - } - -}; - -module.exports = internalHost; diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js deleted file mode 100644 index edf5c3a0..00000000 --- a/backend/internal/ip_ranges.js +++ /dev/null @@ -1,147 +0,0 @@ -const https = require('https'); -const fs = require('fs'); -const logger = require('../logger').ip_ranges; -const error = require('../lib/error'); -const internalNginx = require('./nginx'); -const { Liquid } = require('liquidjs'); - -const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json'; -const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4'; -const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6'; - -const internalIpRanges = { - - interval_timeout: 1000 * 60 * 60 * 6, // 6 hours - interval: null, - interval_processing: false, - iteration_count: 0, - - initTimer: () => { - logger.info('IP Ranges Renewal Timer initialized'); - internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout); - }, - - fetchUrl: (url) => { - return new Promise((resolve, reject) => { - logger.info('Fetching ' + url); - return https.get(url, (res) => { - res.setEncoding('utf8'); - let raw_data = ''; - res.on('data', (chunk) => { - raw_data += chunk; - }); - - res.on('end', () => { - resolve(raw_data); - }); - }).on('error', (err) => { - reject(err); - }); - }); - }, - - /** - * Triggered at startup and then later by a timer, this will fetch the ip ranges from services and apply them to nginx. - */ - fetch: () => { - if (!internalIpRanges.interval_processing) { - internalIpRanges.interval_processing = true; - logger.info('Fetching IP Ranges from online services...'); - - let ip_ranges = []; - - return internalIpRanges.fetchUrl(CLOUDFRONT_URL) - .then((cloudfront_data) => { - let data = JSON.parse(cloudfront_data); - - if (data && typeof data.prefixes !== 'undefined') { - data.prefixes.map((item) => { - if (item.service === 'CLOUDFRONT') { - ip_ranges.push(item.ip_prefix); - } - }); - } - - if (data && typeof data.ipv6_prefixes !== 'undefined') { - data.ipv6_prefixes.map((item) => { - if (item.service === 'CLOUDFRONT') { - ip_ranges.push(item.ipv6_prefix); - } - }); - } - }) - .then(() => { - return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL); - }) - .then((cloudfare_data) => { - let items = cloudfare_data.split('\n'); - ip_ranges = [... ip_ranges, ... items]; - }) - .then(() => { - return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); - }) - .then((cloudfare_data) => { - let items = cloudfare_data.split('\n'); - ip_ranges = [... ip_ranges, ... items]; - }) - .then(() => { - let clean_ip_ranges = []; - ip_ranges.map((range) => { - if (range) { - clean_ip_ranges.push(range); - } - }); - - return internalIpRanges.generateConfig(clean_ip_ranges) - .then(() => { - if (internalIpRanges.iteration_count) { - // Reload nginx - return internalNginx.reload(); - } - }); - }) - .then(() => { - internalIpRanges.interval_processing = false; - internalIpRanges.iteration_count++; - }) - .catch((err) => { - logger.error(err.message); - internalIpRanges.interval_processing = false; - }); - } - }, - - /** - * @param {Array} ip_ranges - * @returns {Promise} - */ - generateConfig: (ip_ranges) => { - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = '/etc/nginx/conf.d/include/ip_ranges.conf'; - try { - template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - renderEngine - .parseAndRender(template, {ip_ranges: ip_ranges}) - .then((config_text) => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - resolve(true); - }) - .catch((err) => { - logger.warn('Could not write ' + filename + ':', err.message); - reject(new error.ConfigurationError(err.message)); - }); - }); - } -}; - -module.exports = internalIpRanges; diff --git a/backend/internal/jwt/jwt.go b/backend/internal/jwt/jwt.go new file mode 100644 index 00000000..4c786f26 --- /dev/null +++ b/backend/internal/jwt/jwt.go @@ -0,0 +1,60 @@ +package jwt + +import ( + "fmt" + "time" + + "npm/internal/entity/user" + "npm/internal/logger" + + "github.com/dgrijalva/jwt-go" +) + +// UserJWTClaims is the structure of a JWT for a User +type UserJWTClaims struct { + UserID int `json:"uid"` + Roles []string `json:"roles"` + jwt.StandardClaims +} + +// GeneratedResponse is the response of a generated token, usually used in http response +type GeneratedResponse struct { + Expires int64 `json:"expires"` + Token string `json:"token"` +} + +// Generate will create a JWT +func Generate(userObj *user.Model) (GeneratedResponse, error) { + var response GeneratedResponse + + key, _ := GetPrivateKey() + expires := time.Now().AddDate(0, 0, 1) // 1 day + + // Create the Claims + claims := UserJWTClaims{ + userObj.ID, + userObj.Roles, + jwt.StandardClaims{ + IssuedAt: time.Now().Unix(), + ExpiresAt: expires.Unix(), + Issuer: "api", + }, + } + + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + var err error + token.Signature, err = token.SignedString(key) + if err != nil { + logger.Error("JWTError", fmt.Errorf("Error signing token: %v", err)) + return response, err + } + + response = GeneratedResponse{ + Expires: expires.Unix(), + Token: token.Signature, + } + + return response, nil +} diff --git a/backend/internal/jwt/keys.go b/backend/internal/jwt/keys.go new file mode 100644 index 00000000..e48c334d --- /dev/null +++ b/backend/internal/jwt/keys.go @@ -0,0 +1,86 @@ +package jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + "npm/internal/config" +) + +var ( + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey +) + +// GetPrivateKey will load the key from config package and return a usable object +// It should only load from file once per program execution +func GetPrivateKey() (*rsa.PrivateKey, error) { + if privateKey == nil { + var blankKey *rsa.PrivateKey + + if config.PrivateKey == "" { + return blankKey, errors.New("Could not get Private Key from configuration") + } + + var err error + privateKey, err = LoadPemPrivateKey(config.PrivateKey) + if err != nil { + return blankKey, err + } + } + + pub, pubErr := GetPublicKey() + if pubErr != nil { + return privateKey, pubErr + } + + privateKey.PublicKey = *pub + + return privateKey, pubErr +} + +// GetPublicKey will load the key from config package and return a usable object +// It should only load once per program execution +func GetPublicKey() (*rsa.PublicKey, error) { + if publicKey == nil { + var blankKey *rsa.PublicKey + + if config.PublicKey == "" { + return blankKey, errors.New("Could not get Public Key filename, check environment variables") + } + + var err error + publicKey, err = LoadPemPublicKey(config.PublicKey) + if err != nil { + return blankKey, err + } + } + + return publicKey, nil +} + +// LoadPemPrivateKey reads a key from a PEM encoded string and returns a private key +func LoadPemPrivateKey(content string) (*rsa.PrivateKey, error) { + var key *rsa.PrivateKey + data, _ := pem.Decode([]byte(content)) + var err error + key, err = x509.ParsePKCS1PrivateKey(data.Bytes) + if err != nil { + return key, err + } + return key, nil +} + +// LoadPemPublicKey reads a key from a PEM encoded string and returns a public key +func LoadPemPublicKey(content string) (*rsa.PublicKey, error) { + var key *rsa.PublicKey + data, _ := pem.Decode([]byte(content)) + publicKeyFileImported, err := x509.ParsePKCS1PublicKey(data.Bytes) + if err != nil { + return key, err + } + + return publicKeyFileImported, nil +} diff --git a/backend/internal/logger/config.go b/backend/internal/logger/config.go new file mode 100644 index 00000000..c0f8a35a --- /dev/null +++ b/backend/internal/logger/config.go @@ -0,0 +1,40 @@ +package logger + +import "github.com/getsentry/sentry-go" + +// Level type +type Level int + +// Log level definitions +const ( + // DebugLevel usually only enabled when debugging. Very verbose logging. + DebugLevel Level = 10 + // InfoLevel general operational entries about what's going on inside the application. + InfoLevel Level = 20 + // WarnLevel non-critical entries that deserve eyes. + WarnLevel Level = 30 + // ErrorLevel used for errors that should definitely be noted. + ErrorLevel Level = 40 +) + +// Config options for the logger. +type Config struct { + LogThreshold Level + Formatter string + SentryConfig sentry.ClientOptions +} + +// Interface for a logger +type Interface interface { + GetLogLevel() Level + Debug(format string, args ...interface{}) + Info(format string, args ...interface{}) + Warn(format string, args ...interface{}) + Error(errorClass string, err error, args ...interface{}) + Errorf(errorClass, format string, err error, args ...interface{}) +} + +// ConfigurableLogger is an interface for a logger that can be configured +type ConfigurableLogger interface { + Configure(c *Config) error +} diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go new file mode 100644 index 00000000..f82145ee --- /dev/null +++ b/backend/internal/logger/logger.go @@ -0,0 +1,242 @@ +package logger + +import ( + "encoding/json" + "fmt" + stdlog "log" + "os" + "runtime/debug" + "sync" + "time" + + "github.com/fatih/color" + "github.com/getsentry/sentry-go" +) + +var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color + +// Log message structure. +type Log struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + Pid int `json:"pid"` + Summary string `json:"summary,omitempty"` + Caller string `json:"caller,omitempty"` + StackTrace []string `json:"stack_trace,omitempty"` +} + +// Logger instance +type Logger struct { + Config + mux sync.Mutex +} + +// global logging configuration. +var logger = NewLogger() + +// NewLogger creates a new logger instance +func NewLogger() *Logger { + color.NoColor = false + colorReset = color.New(color.Reset) + colorGray = color.New(color.FgWhite) + colorYellow = color.New(color.Bold, color.FgYellow) + colorBlue = color.New(color.Bold, color.FgBlue) + colorRed = color.New(color.Bold, color.FgRed) + colorMagenta = color.New(color.Bold, color.FgMagenta) + colorBlack = color.New(color.Bold, color.FgBlack) + colorWhite = color.New(color.Bold, color.FgWhite) + + return &Logger{ + Config: NewConfig(), + } +} + +// NewConfig returns the default config +func NewConfig() Config { + return Config{ + LogThreshold: InfoLevel, + Formatter: "json", + } +} + +// Configure logger and will return error if missing required fields. +func Configure(c *Config) error { + return logger.Configure(c) +} + +// GetLogLevel currently configured +func GetLogLevel() Level { + return logger.GetLogLevel() +} + +// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf. +func Debug(format string, args ...interface{}) { + logger.Debug(format, args...) +} + +// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf. +func Info(format string, args ...interface{}) { + logger.Info(format, args...) +} + +// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf. +func Warn(format string, args ...interface{}) { + logger.Warn(format, args...) +} + +// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged. +// Attempts to log to bugsang. +func Error(errorClass string, err error) { + logger.Error(errorClass, err) +} + +// Configure logger and will return error if missing required fields. +func (l *Logger) Configure(c *Config) error { + // ensure updates to the config are atomic + l.mux.Lock() + defer l.mux.Unlock() + + if c == nil { + return fmt.Errorf("a non nil Config is mandatory") + } + + if err := c.LogThreshold.validate(); err != nil { + return err + } + + l.LogThreshold = c.LogThreshold + l.Formatter = c.Formatter + l.SentryConfig = c.SentryConfig + + if c.SentryConfig.Dsn != "" { + if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil { + fmt.Printf("Sentry initialization failed: %v\n", sentryErr) + } + } + + stdlog.SetFlags(0) // this removes timestamp prefixes from logs + return nil +} + +// validate the log level is in the accepted list. +func (l Level) validate() error { + switch l { + case DebugLevel, InfoLevel, WarnLevel, ErrorLevel: + return nil + default: + return fmt.Errorf("invalid \"Level\" %d", l) + } +} + +var logLevels = map[Level]string{ + DebugLevel: "DEBUG", + InfoLevel: "INFO", + WarnLevel: "WARN", + ErrorLevel: "ERROR", +} + +func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) { + if logLevel < l.LogThreshold { + return + } + + errorClass := "" + if logLevel == ErrorLevel { + // First arg is the errorClass + errorClass = args[0].(string) + if len(args) > 1 { + args = args[1:] + } else { + args = []interface{}{} + } + } + + stringMessage := fmt.Sprintf(format, args...) + + if l.Formatter == "json" { + // JSON Log Format + jsonLog, _ := json.Marshal( + Log{ + Timestamp: time.Now().Format(time.RFC3339Nano), + Level: logLevels[logLevel], + Message: stringMessage, + Pid: os.Getpid(), + }, + ) + + stdlog.Println(string(jsonLog)) + } else { + // Nice Log Format + var colorLevel *color.Color + switch logLevel { + case DebugLevel: + colorLevel = colorMagenta + case InfoLevel: + colorLevel = colorBlue + case WarnLevel: + colorLevel = colorYellow + case ErrorLevel: + colorLevel = colorRed + stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage) + } + + t := time.Now() + stdlog.Println( + colorBlack.Sprint("["), + colorWhite.Sprint(t.Format("2006-01-02 15:04:05")), + colorBlack.Sprint("] "), + colorLevel.Sprintf("%-8v", logLevels[logLevel]), + colorGray.Sprint(stringMessage), + colorReset.Sprint(""), + ) + + if logLevel == ErrorLevel && l.LogThreshold == DebugLevel { + // Print a stack trace too + debug.PrintStack() + } + } +} + +// GetLogLevel currently configured +func (l *Logger) GetLogLevel() Level { + return l.LogThreshold +} + +// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Debug(format string, args ...interface{}) { + l.logLevel(DebugLevel, format, args...) +} + +// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Info(format string, args ...interface{}) { + l.logLevel(InfoLevel, format, args...) +} + +// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf. +func (l *Logger) Warn(format string, args ...interface{}) { + l.logLevel(WarnLevel, format, args...) +} + +// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged. +// Attempts to log to bugsang. +func (l *Logger) Error(errorClass string, err error) { + l.logLevel(ErrorLevel, err.Error(), errorClass) + l.notifySentry(errorClass, err) +} + +func (l *Logger) notifySentry(errorClass string, err error) { + if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" { + + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelError) + scope.SetTag("service", "backend") + scope.SetTag("error_class", errorClass) + }) + + sentry.CaptureException(err) + // Since sentry emits events in the background we need to make sure + // they are sent before we shut down + sentry.Flush(time.Second * 5) + } +} diff --git a/backend/internal/logger/logger_test.go b/backend/internal/logger/logger_test.go new file mode 100644 index 00000000..e0019696 --- /dev/null +++ b/backend/internal/logger/logger_test.go @@ -0,0 +1,168 @@ +package logger + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "testing" + + "github.com/getsentry/sentry-go" + "github.com/stretchr/testify/assert" +) + +func TestGetLogLevel(t *testing.T) { + assert.Equal(t, InfoLevel, GetLogLevel()) +} + +func TestThreshold(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Debug("this should not display") + assert.Empty(t, buf.String()) + + Info("this should display") + assert.NotEmpty(t, buf.String()) + + Error("ErrorClass", errors.New("this should display")) + assert.NotEmpty(t, buf.String()) +} + +func TestDebug(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: DebugLevel, + })) + + Debug("This is a %s message", "test") + assert.Contains(t, buf.String(), "DEBUG") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestInfo(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Info("This is a %s message", "test") + assert.Contains(t, buf.String(), "INFO") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestWarn(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: InfoLevel, + })) + + Warn("This is a %s message", "test") + assert.Contains(t, buf.String(), "WARN") + assert.Contains(t, buf.String(), "This is a test message") +} + +func TestError(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: ErrorLevel, + })) + + Error("TestErrorClass", fmt.Errorf("this is a %s error", "test")) + assert.Contains(t, buf.String(), "ERROR") + assert.Contains(t, buf.String(), "this is a test error") +} + +func TestConfigure(t *testing.T) { + type args struct { + c *Config + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "configure", + args: args{ + &Config{ + LogThreshold: InfoLevel, + SentryConfig: sentry.ClientOptions{}, + }, + }, + wantErr: false, + }, + { + name: "invalid log level", + args: args{ + &Config{ + SentryConfig: sentry.ClientOptions{}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if err := Configure(tt.args.c); (err != nil) != tt.wantErr { + t.Errorf("Configure() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func BenchmarkLogLevelBelowThreshold(b *testing.B) { + l := NewLogger() + + log.SetOutput(ioutil.Discard) + defer func() { + log.SetOutput(os.Stderr) + }() + + for i := 0; i < b.N; i++ { + l.logLevel(DebugLevel, "benchmark %d", i) + } +} + +func BenchmarkLogLevelAboveThreshold(b *testing.B) { + l := NewLogger() + + log.SetOutput(ioutil.Discard) + defer func() { + log.SetOutput(os.Stderr) + }() + + for i := 0; i < b.N; i++ { + l.logLevel(InfoLevel, "benchmark %d", i) + } +} diff --git a/backend/internal/model/filter.go b/backend/internal/model/filter.go new file mode 100644 index 00000000..8b88b70c --- /dev/null +++ b/backend/internal/model/filter.go @@ -0,0 +1,8 @@ +package model + +// Filter is the structure of a field/modifier/value item +type Filter struct { + Field string `json:"field"` + Modifier string `json:"modifier"` + Value []string `json:"value"` +} diff --git a/backend/internal/model/pageinfo.go b/backend/internal/model/pageinfo.go new file mode 100644 index 00000000..129a0a14 --- /dev/null +++ b/backend/internal/model/pageinfo.go @@ -0,0 +1,22 @@ +package model + +import ( + "time" +) + +// PageInfo is the model used by Api Handlers and passed on to other parts +// of the application +type PageInfo struct { + FromDate time.Time `json:"from_date"` + ToDate time.Time `json:"to_date"` + Sort []Sort `json:"sort"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Expand []string `json:"expand"` +} + +// Sort ... +type Sort struct { + Field string `json:"field"` + Direction string `json:"direction"` +} diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js deleted file mode 100644 index 52bdd66d..00000000 --- a/backend/internal/nginx.js +++ /dev/null @@ -1,435 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const logger = require('../logger').nginx; -const utils = require('../lib/utils'); -const error = require('../lib/error'); -const { Liquid } = require('liquidjs'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; - -const internalNginx = { - - /** - * This will: - * - test the nginx config first to make sure it's OK - * - create / recreate the config for the host - * - test again - * - IF OK: update the meta with online status - * - IF BAD: update the meta with offline status and remove the config entirely - * - then reload nginx - * - * @param {Object|String} model - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - configure: (model, host_type, host) => { - let combined_meta = {}; - - return internalNginx.test() - .then(() => { - // Nginx is OK - // We're deleting this config regardless. - return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all - }) - .then(() => { - return internalNginx.generateConfig(host_type, host); - }) - .then(() => { - // Test nginx again and update meta with result - return internalNginx.test() - .then(() => { - // nginx is ok - combined_meta = _.assign({}, host.meta, { - nginx_online: true, - nginx_err: null - }); - - return model - .query() - .where('id', host.id) - .patch({ - meta: combined_meta - }); - }) - .catch((err) => { - // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. - // It will always look like this: - // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) - - let valid_lines = []; - let err_lines = err.message.split('\n'); - err_lines.map(function (line) { - if (line.indexOf('/var/log/nginx/error.log') === -1) { - valid_lines.push(line); - } - }); - - if (debug_mode) { - logger.error('Nginx test failed:', valid_lines.join('\n')); - } - - // config is bad, update meta and delete config - combined_meta = _.assign({}, host.meta, { - nginx_online: false, - nginx_err: valid_lines.join('\n') - }); - - return model - .query() - .where('id', host.id) - .patch({ - meta: combined_meta - }) - .then(() => { - return internalNginx.deleteConfig(host_type, host, true); - }); - }); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - return combined_meta; - }); - }, - - /** - * @returns {Promise} - */ - test: () => { - if (debug_mode) { - logger.info('Testing Nginx configuration'); - } - - return utils.exec('/usr/sbin/nginx -t -g "error_log off;"'); - }, - - /** - * @returns {Promise} - */ - reload: () => { - return internalNginx.test() - .then(() => { - logger.info('Reloading Nginx'); - return utils.exec('/usr/sbin/nginx -s reload'); - }); - }, - - /** - * @param {String} host_type - * @param {Integer} host_id - * @returns {String} - */ - getConfigName: (host_type, host_id) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - if (host_type === 'default') { - return '/data/nginx/default_host/site.conf'; - } - - return '/data/nginx/' + host_type + '/' + host_id + '.conf'; - }, - - /** - * Generates custom locations - * @param {Object} host - * @returns {Promise} - */ - renderLocations: (host) => { - - //logger.info('host = ' + JSON.stringify(host, null, 2)); - return new Promise((resolve, reject) => { - let template; - - try { - template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - let renderer = new Liquid({ - root: __dirname + '/../templates/' - }); - let renderedLocations = ''; - - const locationRendering = async () => { - for (let i = 0; i < host.locations.length; i++) { - let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id}, - {ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits}, - {allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support}, - {hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list}, - {certificate: host.certificate}, host.locations[i]); - - if (locationCopy.forward_host.indexOf('/') > -1) { - const splitted = locationCopy.forward_host.split('/'); - - locationCopy.forward_host = splitted.shift(); - locationCopy.forward_path = `/${splitted.join('/')}`; - } - - //logger.info('locationCopy = ' + JSON.stringify(locationCopy, null, 2)); - - // eslint-disable-next-line - renderedLocations += await renderer.parseAndRender(template, locationCopy); - } - - }; - - locationRendering().then(() => resolve(renderedLocations)); - - }); - }, - - /** - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - generateConfig: (host_type, host) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - if (debug_mode) { - logger.info('Generating ' + host_type + ' Config:', host); - } - - // logger.info('host = ' + JSON.stringify(host, null, 2)); - - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = internalNginx.getConfigName(host_type, host.id); - - try { - template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - let locationsPromise; - let origLocations; - - // Manipulate the data a bit before sending it to the template - if (host_type !== 'default') { - host.use_default_location = true; - if (typeof host.advanced_config !== 'undefined' && host.advanced_config) { - host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); - } - } - - if (host.locations) { - //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); - origLocations = [].concat(host.locations); - locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { - host.locations = renderedLocations; - }); - - // Allow someone who is using / custom location path to use it, and skip the default / location - _.map(host.locations, (location) => { - if (location.path === '/') { - host.use_default_location = false; - } - }); - - } else { - locationsPromise = Promise.resolve(); - } - - // Set the IPv6 setting for the host - host.ipv6 = internalNginx.ipv6Enabled(); - - locationsPromise.then(() => { - renderEngine - .parseAndRender(template, host) - .then((config_text) => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } - - // Restore locations array - host.locations = origLocations; - - resolve(true); - }) - .catch((err) => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); - }); - }); - }); - }, - - /** - * This generates a temporary nginx config listening on port 80 for the domain names listed - * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt - * when requesting a certificate without having a hostname set up already. - * - * @param {Object} certificate - * @returns {Promise} - */ - generateLetsEncryptRequestConfig: (certificate) => { - if (debug_mode) { - logger.info('Generating LetsEncrypt Request Config:', certificate); - } - - let renderEngine = new Liquid({ - root: __dirname + '/../templates/' - }); - - return new Promise((resolve, reject) => { - let template = null; - let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; - - try { - template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'}); - } catch (err) { - reject(new error.ConfigurationError(err.message)); - return; - } - - certificate.ipv6 = internalNginx.ipv6Enabled(); - - renderEngine - .parseAndRender(template, certificate) - .then((config_text) => { - fs.writeFileSync(filename, config_text, {encoding: 'utf8'}); - - if (debug_mode) { - logger.success('Wrote config:', filename, config_text); - } - - resolve(true); - }) - .catch((err) => { - if (debug_mode) { - logger.warn('Could not write ' + filename + ':', err.message); - } - - reject(new error.ConfigurationError(err.message)); - }); - }); - }, - - /** - * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` - * - * @param {Object} certificate - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - deleteLetsEncryptRequestConfig: (certificate, throw_errors) => { - return new Promise((resolve, reject) => { - try { - let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; - - if (debug_mode) { - logger.warn('Deleting nginx config: ' + config_file); - } - - fs.unlinkSync(config_file); - } catch (err) { - if (debug_mode) { - logger.warn('Could not delete config:', err.message); - } - - if (throw_errors) { - reject(err); - } - } - - resolve(); - }); - }, - - /** - * @param {String} host_type - * @param {Object} [host] - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - deleteConfig: (host_type, host, throw_errors) => { - host_type = host_type.replace(new RegExp('-', 'g'), '_'); - - return new Promise((resolve, reject) => { - try { - let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id); - - if (debug_mode) { - logger.warn('Deleting nginx config: ' + config_file); - } - - fs.unlinkSync(config_file); - } catch (err) { - if (debug_mode) { - logger.warn('Could not delete config:', err.message); - } - - if (throw_errors) { - reject(err); - } - } - - resolve(); - }); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @returns {Promise} - */ - bulkGenerateConfigs: (host_type, hosts) => { - let promises = []; - hosts.map(function (host) { - promises.push(internalNginx.generateConfig(host_type, host)); - }); - - return Promise.all(promises); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @param {Boolean} [throw_errors] - * @returns {Promise} - */ - bulkDeleteConfigs: (host_type, hosts, throw_errors) => { - let promises = []; - hosts.map(function (host) { - promises.push(internalNginx.deleteConfig(host_type, host, throw_errors)); - }); - - return Promise.all(promises); - }, - - /** - * @param {string} config - * @returns {boolean} - */ - advancedConfigHasDefaultLocation: function (config) { - return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); - }, - - /** - * @returns {boolean} - */ - ipv6Enabled: function () { - if (typeof process.env.DISABLE_IPV6 !== 'undefined') { - const disabled = process.env.DISABLE_IPV6.toLowerCase(); - return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes'); - } - - return true; - } -}; - -module.exports = internalNginx; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js deleted file mode 100644 index 09b8bca5..00000000 --- a/backend/internal/proxy-host.js +++ /dev/null @@ -1,466 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const proxyHostModel = require('../models/proxy_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalProxyHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('proxy_hosts:create', data) - .then(() => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return proxyHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalProxyHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // re-fetch with cert - return internalProxyHost.get(access, { - id: row.id, - expand: ['certificate', 'owner', 'access_list.[clients,items]'] - }); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(proxyHostModel, 'proxy_host', row) - .then(() => { - return row; - }); - }) - .then((row) => { - // Audit log - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) - .then(() => { - return row; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @return {Promise} - */ - update: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('proxy_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalProxyHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return proxyHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'proxy-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalProxyHost.get(access, { - id: data.id, - expand: ['owner', 'certificate', 'access_list.[clients,items]'] - }) - .then((row) => { - if (!row.enabled) { - // No need to add nginx config if host is disabled - return row; - } - // Configure nginx - return internalNginx.configure(proxyHostModel, 'proxy_host', row) - .then((new_meta) => { - row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('proxy_hosts:get', data.id) - .then((access_data) => { - let query = proxyHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,access_list,access_list.[clients,items],certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('proxy_hosts:delete', data.id) - .then(() => { - return internalProxyHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return proxyHostModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('proxy_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'proxy-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - enable: (access, data) => { - return access.can('proxy_hosts:update', data.id) - .then(() => { - return internalProxyHost.get(access, { - id: data.id, - expand: ['certificate', 'owner', 'access_list'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); - } - - row.enabled = 1; - - return proxyHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 1 - }) - .then(() => { - // Configure nginx - return internalNginx.configure(proxyHostModel, 'proxy_host', row); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'enabled', - object_type: 'proxy-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - disable: (access, data) => { - return access.can('proxy_hosts:update', data.id) - .then(() => { - return internalProxyHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); - } - - row.enabled = 0; - - return proxyHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 0 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('proxy_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'disabled', - object_type: 'proxy-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Hosts - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('proxy_hosts:list') - .then((access_data) => { - let query = proxyHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,access_list,certificate]') - .orderBy('domain_names', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = proxyHostModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - } -}; - -module.exports = internalProxyHost; diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js deleted file mode 100644 index f22c3668..00000000 --- a/backend/internal/redirection-host.js +++ /dev/null @@ -1,461 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const redirectionHostModel = require('../models/redirection_host'); -const internalHost = require('./host'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); -const internalCertificate = require('./certificate'); - -function omissions () { - return ['is_deleted']; -} - -const internalRedirectionHost = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('redirection_hosts:create', data) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - }) - .then(() => { - // At this point the domains should have been checked - data.owner_user_id = access.token.getUserId(1); - data = internalHost.cleanSslHstsData(data); - - return redirectionHostModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, data) - .then((cert) => { - // update host with cert id - return internalRedirectionHost.update(access, { - id: row.id, - certificate_id: cert.id - }); - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // re-fetch with cert - return internalRedirectionHost.get(access, { - id: row.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(redirectionHostModel, 'redirection_host', row) - .then(() => { - return row; - }); - }) - .then((row) => { - data.meta = _.assign({}, data.meta || {}, row.meta); - - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'redirection-host', - object_id: row.id, - meta: data - }) - .then(() => { - return row; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @return {Promise} - */ - update: (access, data) => { - let create_certificate = data.certificate_id === 'new'; - - if (create_certificate) { - delete data.certificate_id; - } - - return access.can('redirection_hosts:update', data.id) - .then((/*access_data*/) => { - // Get a list of the domain names and check each of them against existing records - let domain_name_check_promises = []; - - if (typeof data.domain_names !== 'undefined') { - data.domain_names.map(function (domain_name) { - domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id)); - }); - - return Promise.all(domain_name_check_promises) - .then((check_results) => { - check_results.map(function (result) { - if (result.is_taken) { - throw new error.ValidationError(result.hostname + ' is already in use'); - } - }); - }); - } - }) - .then(() => { - return internalRedirectionHost.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - if (create_certificate) { - return internalCertificate.createQuickCertificate(access, { - domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta) - }) - .then((cert) => { - // update host with cert id - data.certificate_id = cert.id; - }) - .then(() => { - return row; - }); - } else { - return row; - } - }) - .then((row) => { - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - data = _.assign({}, { - domain_names: row.domain_names - }, data); - - data = internalHost.cleanSslHstsData(data, row); - - return redirectionHostModel - .query() - .where({id: data.id}) - .patch(data) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'redirection-host', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }) - .then(() => { - return internalRedirectionHost.get(access, { - id: data.id, - expand: ['owner', 'certificate'] - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(redirectionHostModel, 'redirection_host', row) - .then((new_meta) => { - row.meta = new_meta; - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('redirection_hosts:get', data.id) - .then((access_data) => { - let query = redirectionHostModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner,certificate]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - row = internalHost.cleanRowCertificateMeta(row); - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('redirection_hosts:delete', data.id) - .then(() => { - return internalRedirectionHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return redirectionHostModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('redirection_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'redirection-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - enable: (access, data) => { - return access.can('redirection_hosts:update', data.id) - .then(() => { - return internalRedirectionHost.get(access, { - id: data.id, - expand: ['certificate', 'owner'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); - } - - row.enabled = 1; - - return redirectionHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 1 - }) - .then(() => { - // Configure nginx - return internalNginx.configure(redirectionHostModel, 'redirection_host', row); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'enabled', - object_type: 'redirection-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - disable: (access, data) => { - return access.can('redirection_hosts:update', data.id) - .then(() => { - return internalRedirectionHost.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); - } - - row.enabled = 0; - - return redirectionHostModel - .query() - .where('id', row.id) - .patch({ - enabled: 0 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('redirection_host', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'disabled', - object_type: 'redirection-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Hosts - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('redirection_hosts:list') - .then((access_data) => { - let query = redirectionHostModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner,certificate]') - .orderBy('domain_names', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('domain_names', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }) - .then((rows) => { - if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = redirectionHostModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - } -}; - -module.exports = internalRedirectionHost; diff --git a/backend/internal/report.js b/backend/internal/report.js deleted file mode 100644 index 4dde659b..00000000 --- a/backend/internal/report.js +++ /dev/null @@ -1,38 +0,0 @@ -const internalProxyHost = require('./proxy-host'); -const internalRedirectionHost = require('./redirection-host'); -const internalDeadHost = require('./dead-host'); -const internalStream = require('./stream'); - -const internalReport = { - - /** - * @param {Access} access - * @return {Promise} - */ - getHostsReport: (access) => { - return access.can('reports:hosts', 1) - .then((access_data) => { - let user_id = access.token.getUserId(1); - - let promises = [ - internalProxyHost.getCount(user_id, access_data.visibility), - internalRedirectionHost.getCount(user_id, access_data.visibility), - internalStream.getCount(user_id, access_data.visibility), - internalDeadHost.getCount(user_id, access_data.visibility) - ]; - - return Promise.all(promises); - }) - .then((counts) => { - return { - proxy: counts.shift(), - redirection: counts.shift(), - stream: counts.shift(), - dead: counts.shift() - }; - }); - - } -}; - -module.exports = internalReport; diff --git a/backend/internal/setting.js b/backend/internal/setting.js deleted file mode 100644 index d4ac67d8..00000000 --- a/backend/internal/setting.js +++ /dev/null @@ -1,133 +0,0 @@ -const fs = require('fs'); -const error = require('../lib/error'); -const settingModel = require('../models/setting'); -const internalNginx = require('./nginx'); - -const internalSetting = { - - /** - * @param {Access} access - * @param {Object} data - * @param {String} data.id - * @return {Promise} - */ - update: (access, data) => { - return access.can('settings:update', data.id) - .then((/*access_data*/) => { - return internalSetting.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - return settingModel - .query() - .where({id: data.id}) - .patch(data); - }) - .then(() => { - return internalSetting.get(access, { - id: data.id - }); - }) - .then((row) => { - if (row.id === 'default-site') { - // write the html if we need to - if (row.value === 'html') { - fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'}); - } - - // Configure nginx - return internalNginx.deleteConfig('default') - .then(() => { - return internalNginx.generateConfig('default', row); - }) - .then(() => { - return internalNginx.test(); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - return row; - }) - .catch((/*err*/) => { - internalNginx.deleteConfig('default') - .then(() => { - return internalNginx.test(); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - // I'm being slack here I know.. - throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.'); - }); - }); - } else { - return row; - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {String} data.id - * @return {Promise} - */ - get: (access, data) => { - return access.can('settings:get', data.id) - .then(() => { - return settingModel - .query() - .where('id', data.id) - .first(); - }) - .then((row) => { - if (row) { - return row; - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * This will only count the settings - * - * @param {Access} access - * @returns {*} - */ - getCount: (access) => { - return access.can('settings:list') - .then(() => { - return settingModel - .query() - .count('id as count') - .first(); - }) - .then((row) => { - return parseInt(row.count, 10); - }); - }, - - /** - * All settings - * - * @param {Access} access - * @returns {Promise} - */ - getAll: (access) => { - return access.can('settings:list') - .then(() => { - return settingModel - .query() - .orderBy('description', 'ASC'); - }); - } -}; - -module.exports = internalSetting; diff --git a/backend/internal/state/state.go b/backend/internal/state/state.go new file mode 100644 index 00000000..e09867a8 --- /dev/null +++ b/backend/internal/state/state.go @@ -0,0 +1,31 @@ +package state + +import ( + "sync" +) + +// AppState holds pointers to channels and waitGroups +// shared by all goroutines of the application +type AppState struct { + waitGroup sync.WaitGroup + termSig chan bool +} + +// NewState ... +func NewState() *AppState { + state := &AppState{ + // buffered channel + termSig: make(chan bool, 1), + } + return state +} + +// GetWaitGroup ... +func (state *AppState) GetWaitGroup() *sync.WaitGroup { + return &state.waitGroup +} + +// GetTermSig ... +func (state *AppState) GetTermSig() chan bool { + return state.termSig +} diff --git a/backend/internal/stream.js b/backend/internal/stream.js deleted file mode 100644 index 9c458a10..00000000 --- a/backend/internal/stream.js +++ /dev/null @@ -1,348 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const streamModel = require('../models/stream'); -const internalNginx = require('./nginx'); -const internalAuditLog = require('./audit-log'); - -function omissions () { - return ['is_deleted']; -} - -const internalStream = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - return access.can('streams:create', data) - .then((/*access_data*/) => { - // TODO: At this point the existing ports should have been checked - data.owner_user_id = access.token.getUserId(1); - - if (typeof data.meta === 'undefined') { - data.meta = {}; - } - - return streamModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((row) => { - // Configure nginx - return internalNginx.configure(streamModel, 'stream', row) - .then(() => { - return internalStream.get(access, {id: row.id, expand: ['owner']}); - }); - }) - .then((row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'stream', - object_id: row.id, - meta: data - }) - .then(() => { - return row; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @return {Promise} - */ - update: (access, data) => { - return access.can('streams:update', data.id) - .then((/*access_data*/) => { - // TODO: at this point the existing streams should have been checked - return internalStream.get(access, {id: data.id}); - }) - .then((row) => { - if (row.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); - } - - return streamModel - .query() - .omit(omissions()) - .patchAndFetchById(row.id, data) - .then((saved_row) => { - return internalNginx.configure(streamModel, 'stream', saved_row) - .then(() => { - return internalStream.get(access, {id: row.id, expand: ['owner']}); - }); - }) - .then((saved_row) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'stream', - object_id: row.id, - meta: data - }) - .then(() => { - return _.omit(saved_row, omissions()); - }); - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - return access.can('streams:get', data.id) - .then((access_data) => { - let query = streamModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[owner]') - .first(); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('streams:delete', data.id) - .then(() => { - return internalStream.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } - - return streamModel - .query() - .where('id', row.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('stream', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'stream', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - enable: (access, data) => { - return access.can('streams:update', data.id) - .then(() => { - return internalStream.get(access, { - id: data.id, - expand: ['owner'] - }); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (row.enabled) { - throw new error.ValidationError('Host is already enabled'); - } - - row.enabled = 1; - - return streamModel - .query() - .where('id', row.id) - .patch({ - enabled: 1 - }) - .then(() => { - // Configure nginx - return internalNginx.configure(streamModel, 'stream', row); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'enabled', - object_type: 'stream', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Number} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - disable: (access, data) => { - return access.can('streams:update', data.id) - .then(() => { - return internalStream.get(access, {id: data.id}); - }) - .then((row) => { - if (!row) { - throw new error.ItemNotFoundError(data.id); - } else if (!row.enabled) { - throw new error.ValidationError('Host is already disabled'); - } - - row.enabled = 0; - - return streamModel - .query() - .where('id', row.id) - .patch({ - enabled: 0 - }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig('stream', row) - .then(() => { - return internalNginx.reload(); - }); - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'disabled', - object_type: 'stream-host', - object_id: row.id, - meta: _.omit(row, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * All Streams - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('streams:list') - .then((access_data) => { - let query = streamModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[owner]') - .orderBy('incoming_port', 'ASC'); - - if (access_data.permission_visibility !== 'all') { - query.andWhere('owner_user_id', access.token.getUserId(1)); - } - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('incoming_port', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * Report use - * - * @param {Number} user_id - * @param {String} visibility - * @returns {Promise} - */ - getCount: (user_id, visibility) => { - let query = streamModel - .query() - .count('id as count') - .where('is_deleted', 0); - - if (visibility !== 'all') { - query.andWhere('owner_user_id', user_id); - } - - return query.first() - .then((row) => { - return parseInt(row.count, 10); - }); - } -}; - -module.exports = internalStream; diff --git a/backend/internal/token.js b/backend/internal/token.js deleted file mode 100644 index a64b9010..00000000 --- a/backend/internal/token.js +++ /dev/null @@ -1,162 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const authModel = require('../models/auth'); -const helpers = require('../lib/helpers'); -const TokenModel = require('../models/token'); - -module.exports = { - - /** - * @param {Object} data - * @param {String} data.identity - * @param {String} data.secret - * @param {String} [data.scope] - * @param {String} [data.expiry] - * @param {String} [issuer] - * @returns {Promise} - */ - getTokenFromEmail: (data, issuer) => { - let Token = new TokenModel(); - - data.scope = data.scope || 'user'; - data.expiry = data.expiry || '1d'; - - return userModel - .query() - .where('email', data.identity) - .andWhere('is_deleted', 0) - .andWhere('is_disabled', 0) - .first() - .then((user) => { - if (user) { - // Get auth - return authModel - .query() - .where('user_id', '=', user.id) - .where('type', '=', 'password') - .first() - .then((auth) => { - if (auth) { - return auth.verifyPassword(data.secret) - .then((valid) => { - if (valid) { - - if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) { - // The scope requested doesn't exist as a role against the user, - // you shall not pass. - throw new error.AuthError('Invalid scope: ' + data.scope); - } - - // Create a moment of the expiry expression - let expiry = helpers.parseDatePeriod(data.expiry); - if (expiry === null) { - throw new error.AuthError('Invalid expiry time: ' + data.expiry); - } - - return Token.create({ - iss: issuer || 'api', - attrs: { - id: user.id - }, - scope: [data.scope], - expiresIn: data.expiry - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString() - }; - }); - } else { - throw new error.AuthError('Invalid password'); - } - }); - } else { - throw new error.AuthError('No password auth for user'); - } - }); - } else { - throw new error.AuthError('No relevant user found'); - } - }); - }, - - /** - * @param {Access} access - * @param {Object} [data] - * @param {String} [data.expiry] - * @param {String} [data.scope] Only considered if existing token scope is admin - * @returns {Promise} - */ - getFreshToken: (access, data) => { - let Token = new TokenModel(); - - data = data || {}; - data.expiry = data.expiry || '1d'; - - if (access && access.token.getUserId(0)) { - - // Create a moment of the expiry expression - let expiry = helpers.parseDatePeriod(data.expiry); - if (expiry === null) { - throw new error.AuthError('Invalid expiry time: ' + data.expiry); - } - - let token_attrs = { - id: access.token.getUserId(0) - }; - - // Only admins can request otherwise scoped tokens - let scope = access.token.get('scope'); - if (data.scope && access.token.hasScope('admin')) { - scope = [data.scope]; - - if (data.scope === 'job-board' || data.scope === 'worker') { - token_attrs.id = 0; - } - } - - return Token.create({ - iss: 'api', - scope: scope, - attrs: token_attrs, - expiresIn: data.expiry - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString() - }; - }); - } else { - throw new error.AssertionFailedError('Existing token contained invalid user data'); - } - }, - - /** - * @param {Object} user - * @returns {Promise} - */ - getTokenFromUser: (user) => { - const expire = '1d'; - const Token = new TokenModel(); - const expiry = helpers.parseDatePeriod(expire); - - return Token.create({ - iss: 'api', - attrs: { - id: user.id - }, - scope: ['user'], - expiresIn: expire - }) - .then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString(), - user: user - }; - }); - } -}; diff --git a/backend/internal/types/db_date.go b/backend/internal/types/db_date.go new file mode 100644 index 00000000..9339868e --- /dev/null +++ b/backend/internal/types/db_date.go @@ -0,0 +1,39 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +// DBDate is a date time +// type DBDate time.Time +type DBDate struct { + Time time.Time +} + +// Value encodes the type ready for the database +func (d DBDate) Value() (driver.Value, error) { + return driver.Value(d.Time.Unix()), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *DBDate) Scan(src interface{}) error { + d.Time = time.Unix(src.(int64), 0) + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *DBDate) UnmarshalJSON(data []byte) error { + var u int64 + if err := json.Unmarshal(data, &u); err != nil { + return err + } + d.Time = time.Unix(u, 0) + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d DBDate) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Time.Unix()) +} diff --git a/backend/internal/types/jsonb.go b/backend/internal/types/jsonb.go new file mode 100644 index 00000000..d0fa2ae5 --- /dev/null +++ b/backend/internal/types/jsonb.go @@ -0,0 +1,58 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// JSONB can be anything +type JSONB struct { + Encoded string `json:"decoded"` + Decoded interface{} `json:"encoded"` +} + +// Value encodes the type ready for the database +func (j JSONB) Value() (driver.Value, error) { + json, err := json.Marshal(j.Decoded) + return driver.Value(string(json)), err +} + +// Scan takes data from the database and modifies it for Go Types +func (j *JSONB) Scan(src interface{}) error { + var jsonb JSONB + var srcString string + switch v := src.(type) { + case string: + srcString = src.(string) + case []uint8: + srcString = string(src.([]uint8)) + default: + return fmt.Errorf("Incompatible type for JSONB: %v", v) + } + + jsonb.Encoded = srcString + + if err := json.Unmarshal([]byte(srcString), &jsonb.Decoded); err != nil { + return err + } + + *j = jsonb + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (j *JSONB) UnmarshalJSON(data []byte) error { + var jsonb JSONB + jsonb.Encoded = string(data) + if err := json.Unmarshal(data, &jsonb.Decoded); err != nil { + return err + } + *j = jsonb + return nil +} + +// MarshalJSON will marshal for output in api responses +func (j JSONB) MarshalJSON() ([]byte, error) { + return json.Marshal(j.Decoded) +} diff --git a/backend/internal/types/nullable_db_date.go b/backend/internal/types/nullable_db_date.go new file mode 100644 index 00000000..9a0022c2 --- /dev/null +++ b/backend/internal/types/nullable_db_date.go @@ -0,0 +1,54 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +// NullableDBDate is a date time that can be null in the db +// type DBDate time.Time +type NullableDBDate struct { + Time *time.Time +} + +// Value encodes the type ready for the database +func (d NullableDBDate) Value() (driver.Value, error) { + if d.Time == nil { + return nil, nil + } + return driver.Value(d.Time.Unix()), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBDate) Scan(src interface{}) error { + var tme time.Time + if src != nil { + tme = time.Unix(src.(int64), 0) + } + + d.Time = &tme + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBDate) UnmarshalJSON(data []byte) error { + var t time.Time + var u int64 + if err := json.Unmarshal(data, &u); err != nil { + d.Time = &t + return nil + } + t = time.Unix(u, 0) + d.Time = &t + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBDate) MarshalJSON() ([]byte, error) { + if d.Time == nil || d.Time.IsZero() { + return json.Marshal(nil) + } + + return json.Marshal(d.Time.Unix()) +} diff --git a/backend/internal/types/roles.go b/backend/internal/types/roles.go new file mode 100644 index 00000000..2cb79006 --- /dev/null +++ b/backend/internal/types/roles.go @@ -0,0 +1,36 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// Roles is an array of strings +type Roles []string + +// Value encodes the type ready for the database +func (r Roles) Value() (driver.Value, error) { + roles, err := json.Marshal(r) + return driver.Value(string(roles)), err +} + +// Scan takes data from the database and modifies it for Go Types +func (r *Roles) Scan(src interface{}) error { + var roles Roles + var srcString string + switch v := src.(type) { + case string: + srcString = src.(string) + case []uint8: + srcString = string(src.([]uint8)) + default: + return fmt.Errorf("Incompatible type for Roles: %v", v) + } + + if err := json.Unmarshal([]byte(srcString), &roles); err != nil { + return err + } + *r = roles + return nil +} diff --git a/backend/internal/user.js b/backend/internal/user.js deleted file mode 100644 index 2e2d8abf..00000000 --- a/backend/internal/user.js +++ /dev/null @@ -1,518 +0,0 @@ -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const userPermissionModel = require('../models/user_permission'); -const authModel = require('../models/auth'); -const gravatar = require('gravatar'); -const internalToken = require('./token'); -const internalAuditLog = require('./audit-log'); - -function omissions () { - return ['is_deleted']; -} - -const internalUser = { - - /** - * @param {Access} access - * @param {Object} data - * @returns {Promise} - */ - create: (access, data) => { - let auth = data.auth || null; - delete data.auth; - - data.avatar = data.avatar || ''; - data.roles = data.roles || []; - - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - - return access.can('users:create', data) - .then(() => { - data.avatar = gravatar.url(data.email, {default: 'mm'}); - - return userModel - .query() - .omit(omissions()) - .insertAndFetch(data); - }) - .then((user) => { - if (auth) { - return authModel - .query() - .insert({ - user_id: user.id, - type: auth.type, - secret: auth.secret, - meta: {} - }) - .then(() => { - return user; - }); - } else { - return user; - } - }) - .then((user) => { - // Create permissions row as well - let is_admin = data.roles.indexOf('admin') !== -1; - - return userPermissionModel - .query() - .insert({ - user_id: user.id, - visibility: is_admin ? 'all' : 'user', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage' - }) - .then(() => { - return internalUser.get(access, {id: user.id, expand: ['permissions']}); - }); - }) - .then((user) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'created', - object_type: 'user', - object_id: user.id, - meta: user - }) - .then(() => { - return user; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.email] - * @param {String} [data.name] - * @return {Promise} - */ - update: (access, data) => { - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - - return access.can('users:update', data.id) - .then(() => { - - // Make sure that the user being updated doesn't change their email to another user that is already using it - // 1. get user we want to update - return internalUser.get(access, {id: data.id}) - .then((user) => { - - // 2. if email is to be changed, find other users with that email - if (typeof data.email !== 'undefined') { - data.email = data.email.toLowerCase().trim(); - - if (user.email !== data.email) { - return internalUser.isEmailAvailable(data.email, data.id) - .then((available) => { - if (!available) { - throw new error.ValidationError('Email address already in use - ' + data.email); - } - - return user; - }); - } - } - - // No change to email: - return user; - }); - }) - .then((user) => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); - } - - data.avatar = gravatar.url(data.email || user.email, {default: 'mm'}); - - return userModel - .query() - .omit(omissions()) - .patchAndFetchById(user.id, data) - .then((saved_user) => { - return _.omit(saved_user, omissions()); - }); - }) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: data - }) - .then(() => { - return user; - }); - }); - }, - - /** - * @param {Access} access - * @param {Object} [data] - * @param {Integer} [data.id] Defaults to the token user - * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @return {Promise} - */ - get: (access, data) => { - if (typeof data === 'undefined') { - data = {}; - } - - if (typeof data.id === 'undefined' || !data.id) { - data.id = access.token.getUserId(0); - } - - return access.can('users:get', data.id) - .then(() => { - let query = userModel - .query() - .where('is_deleted', 0) - .andWhere('id', data.id) - .allowEager('[permissions]') - .first(); - - // Custom omissions - if (typeof data.omit !== 'undefined' && data.omit !== null) { - query.omit(data.omit); - } - - if (typeof data.expand !== 'undefined' && data.expand !== null) { - query.eager('[' + data.expand.join(', ') + ']'); - } - - return query; - }) - .then((row) => { - if (row) { - return _.omit(row, omissions()); - } else { - throw new error.ItemNotFoundError(data.id); - } - }); - }, - - /** - * Checks if an email address is available, but if a user_id is supplied, it will ignore checking - * against that user. - * - * @param email - * @param user_id - */ - isEmailAvailable: (email, user_id) => { - let query = userModel - .query() - .where('email', '=', email.toLowerCase().trim()) - .where('is_deleted', 0) - .first(); - - if (typeof user_id !== 'undefined') { - query.where('id', '!=', user_id); - } - - return query - .then((user) => { - return !user; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} [data.reason] - * @returns {Promise} - */ - delete: (access, data) => { - return access.can('users:delete', data.id) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - if (!user) { - throw new error.ItemNotFoundError(data.id); - } - - // Make sure user can't delete themselves - if (user.id === access.token.getUserId(0)) { - throw new error.PermissionError('You cannot delete yourself.'); - } - - return userModel - .query() - .where('id', user.id) - .patch({ - is_deleted: 1 - }) - .then(() => { - // Add to audit log - return internalAuditLog.add(access, { - action: 'deleted', - object_type: 'user', - object_id: user.id, - meta: _.omit(user, omissions()) - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * This will only count the users - * - * @param {Access} access - * @param {String} [search_query] - * @returns {*} - */ - getCount: (access, search_query) => { - return access.can('users:list') - .then(() => { - let query = userModel - .query() - .count('id as count') - .where('is_deleted', 0) - .first(); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('user.name', 'like', '%' + search_query + '%') - .orWhere('user.email', 'like', '%' + search_query + '%'); - }); - } - - return query; - }) - .then((row) => { - return parseInt(row.count, 10); - }); - }, - - /** - * All users - * - * @param {Access} access - * @param {Array} [expand] - * @param {String} [search_query] - * @returns {Promise} - */ - getAll: (access, expand, search_query) => { - return access.can('users:list') - .then(() => { - let query = userModel - .query() - .where('is_deleted', 0) - .groupBy('id') - .omit(['is_deleted']) - .allowEager('[permissions]') - .orderBy('name', 'ASC'); - - // Query is used for searching - if (typeof search_query === 'string') { - query.where(function () { - this.where('name', 'like', '%' + search_query + '%') - .orWhere('email', 'like', '%' + search_query + '%'); - }); - } - - if (typeof expand !== 'undefined' && expand !== null) { - query.eager('[' + expand.join(', ') + ']'); - } - - return query; - }); - }, - - /** - * @param {Access} access - * @param {Integer} [id_requested] - * @returns {[String]} - */ - getUserOmisionsByAccess: (access, id_requested) => { - let response = []; // Admin response - - if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) { - response = ['roles', 'is_deleted']; // Restricted response - } - - return response; - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - * @param {String} data.type - * @param {String} data.secret - * @return {Promise} - */ - setPassword: (access, data) => { - return access.can('users:password', data.id) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); - } - - if (user.id === access.token.getUserId(0)) { - // they're setting their own password. Make sure their current password is correct - if (typeof data.current === 'undefined' || !data.current) { - throw new error.ValidationError('Current password was not supplied'); - } - - return internalToken.getTokenFromEmail({ - identity: user.email, - secret: data.current - }) - .then(() => { - return user; - }); - } - - return user; - }) - .then((user) => { - // Get auth, patch if it exists - return authModel - .query() - .where('user_id', user.id) - .andWhere('type', data.type) - .first() - .then((existing_auth) => { - if (existing_auth) { - // patch - return authModel - .query() - .where('user_id', user.id) - .andWhere('type', data.type) - .patch({ - type: data.type, // This is required for the model to encrypt on save - secret: data.secret - }); - } else { - // insert - return authModel - .query() - .insert({ - user_id: user.id, - type: data.type, - secret: data.secret, - meta: {} - }); - } - }) - .then(() => { - // Add to Audit Log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: { - name: user.name, - password_changed: true, - auth_type: data.type - } - }); - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @return {Promise} - */ - setPermissions: (access, data) => { - return access.can('users:permissions', data.id) - .then(() => { - return internalUser.get(access, {id: data.id}); - }) - .then((user) => { - if (user.id !== data.id) { - // Sanity check that something crazy hasn't happened - throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); - } - - return user; - }) - .then((user) => { - // Get perms row, patch if it exists - return userPermissionModel - .query() - .where('user_id', user.id) - .first() - .then((existing_auth) => { - if (existing_auth) { - // patch - return userPermissionModel - .query() - .where('user_id', user.id) - .patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data)); - } else { - // insert - return userPermissionModel - .query() - .insertAndFetch(_.assign({user_id: user.id}, data)); - } - }) - .then((permissions) => { - // Add to Audit Log - return internalAuditLog.add(access, { - action: 'updated', - object_type: 'user', - object_id: user.id, - meta: { - name: user.name, - permissions: permissions - } - }); - - }); - }) - .then(() => { - return true; - }); - }, - - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - */ - loginAs: (access, data) => { - return access.can('users:loginas', data.id) - .then(() => { - return internalUser.get(access, data); - }) - .then((user) => { - return internalToken.getTokenFromUser(user); - }); - } -}; - -module.exports = internalUser; diff --git a/backend/internal/util/maps.go b/backend/internal/util/maps.go new file mode 100644 index 00000000..1ff211ec --- /dev/null +++ b/backend/internal/util/maps.go @@ -0,0 +1,9 @@ +package util + +// MapContainsKey is fairly self explanatory +func MapContainsKey(dict map[string]interface{}, key string) bool { + if _, ok := dict[key]; ok { + return true + } + return false +} diff --git a/backend/internal/util/maps_test.go b/backend/internal/util/maps_test.go new file mode 100644 index 00000000..fdb5d2ff --- /dev/null +++ b/backend/internal/util/maps_test.go @@ -0,0 +1,45 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type rect struct { + width int + height int +} + +func TestMapContainsKey(t *testing.T) { + var r rect + r.width = 5 + r.height = 5 + m := map[string]interface{}{ + "rect_width": r.width, + "rect_height": r.height, + } + tests := []struct { + name string + pass string + want bool + }{ + { + name: "exists", + pass: "rect_width", + want: true, + }, + { + name: "Does not exist", + pass: "rect_perimeter", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MapContainsKey(m, tt.pass) + + assert.Equal(t, result, tt.want) + }) + } +} diff --git a/backend/internal/util/slices.go b/backend/internal/util/slices.go new file mode 100644 index 00000000..c078f1c8 --- /dev/null +++ b/backend/internal/util/slices.go @@ -0,0 +1,35 @@ +package util + +import ( + "strconv" + "strings" +) + +// SliceContainsItem returns whether the slice given contains the item given +func SliceContainsItem(slice []string, item string) bool { + for _, a := range slice { + if a == item { + return true + } + } + return false +} + +// SliceContainsInt returns whether the slice given contains the item given +func SliceContainsInt(slice []int, item int) bool { + for _, a := range slice { + if a == item { + return true + } + } + return false +} + +// ConvertIntSliceToString returns a comma separated string of all items in the slice +func ConvertIntSliceToString(slice []int) string { + strs := []string{} + for _, item := range slice { + strs = append(strs, strconv.Itoa(item)) + } + return strings.Join(strs, ",") +} diff --git a/backend/internal/util/slices_test.go b/backend/internal/util/slices_test.go new file mode 100644 index 00000000..f2f18714 --- /dev/null +++ b/backend/internal/util/slices_test.go @@ -0,0 +1,92 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSliceContainsItem(t *testing.T) { + type want struct { + result bool + } + tests := []struct { + name string + inputString string + inputArray []string + want want + }{ + { + name: "In array", + inputString: "test", + inputArray: []string{"no", "more", "tests", "test"}, + want: want{ + result: true, + }, + }, + { + name: "Not in array", + inputString: "test", + inputArray: []string{"no", "more", "tests"}, + want: want{ + result: false, + }, + }, + { + name: "Case sensitive", + inputString: "test", + inputArray: []string{"no", "TEST", "more"}, + want: want{ + result: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SliceContainsItem(tt.inputArray, tt.inputString) + assert.Equal(t, tt.want.result, got) + }) + } +} + +func TestSliceContainsInt(t *testing.T) { + type want struct { + result bool + } + tests := []struct { + name string + inputInt int + inputArray []int + want want + }{ + { + name: "In array", + inputInt: 1, + inputArray: []int{1, 2, 3, 4}, + want: want{ + result: true, + }, + }, + { + name: "Not in array", + inputInt: 1, + inputArray: []int{10, 2, 3, 4}, + want: want{ + result: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SliceContainsInt(tt.inputArray, tt.inputInt) + assert.Equal(t, tt.want.result, got) + }) + } +} + +func TestConvertIntSliceToString(t *testing.T) { + items := []int{1, 2, 3, 4, 5, 6, 7} + expectedStr := "1,2,3,4,5,6,7" + str := ConvertIntSliceToString(items) + assert.Equal(t, expectedStr, str) +} diff --git a/backend/internal/validator/hosts.go b/backend/internal/validator/hosts.go new file mode 100644 index 00000000..6453fa49 --- /dev/null +++ b/backend/internal/validator/hosts.go @@ -0,0 +1,23 @@ +package validator + +import ( + "fmt" + + "npm/internal/entity/certificate" + "npm/internal/entity/host" +) + +// ValidateHost will check if associated objects exist and other checks +// will return a nil error if things are OK +func ValidateHost(h host.Model) error { + if h.CertificateID > 0 { + // Check certificate exists and is valid + // This will not determine if the certificate is Ready to use, + // as this validation only cares that the row exists. + if _, cErr := certificate.GetByID(h.CertificateID); cErr != nil { + return fmt.Errorf("Certificate #%d does not exist", h.CertificateID) + } + } + + return nil +} diff --git a/backend/internal/worker/certificate.go b/backend/internal/worker/certificate.go new file mode 100644 index 00000000..157bdb67 --- /dev/null +++ b/backend/internal/worker/certificate.go @@ -0,0 +1,61 @@ +package worker + +import ( + "time" + + "npm/internal/entity/certificate" + "npm/internal/logger" + "npm/internal/state" +) + +type certificateWorker struct { + state *state.AppState +} + +// StartCertificateWorker starts the CertificateWorker +func StartCertificateWorker(state *state.AppState) { + worker := newCertificateWorker(state) + logger.Info("CertificateWorker Started") + worker.Run() +} + +func newCertificateWorker(state *state.AppState) *certificateWorker { + return &certificateWorker{ + state: state, + } +} + +// Run the CertificateWorker +func (w *certificateWorker) Run() { + // global wait group + gwg := w.state.GetWaitGroup() + gwg.Add(1) + + ticker := time.NewTicker(15 * time.Second) +mainLoop: + for { + select { + case _, more := <-w.state.GetTermSig(): + if !more { + logger.Info("Terminating CertificateWorker ... ") + break mainLoop + } + case <-ticker.C: + requestCertificates() + } + } +} + +func requestCertificates() { + rows, err := certificate.GetByStatus(certificate.StatusReady) + if err != nil { + logger.Error("requestCertificatesError", err) + return + } + + for _, row := range rows { + if err := row.Request(); err != nil { + logger.Error("CertificateRequestError", err) + } + } +} diff --git a/backend/knexfile.js b/backend/knexfile.js deleted file mode 100644 index 391ca005..00000000 --- a/backend/knexfile.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - development: { - client: 'mysql', - migrations: { - tableName: 'migrations', - stub: 'lib/migrate_template.js', - directory: 'migrations' - } - }, - - production: { - client: 'mysql', - migrations: { - tableName: 'migrations', - stub: 'lib/migrate_template.js', - directory: 'migrations' - } - } -}; diff --git a/backend/lib/access.js b/backend/lib/access.js deleted file mode 100644 index 9d7329d9..00000000 --- a/backend/lib/access.js +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Some Notes: This is a friggin complicated piece of code. - * - * "scope" in this file means "where did this token come from and what is using it", so 99% of the time - * the "scope" is going to be "user" because it would be a user token. This is not to be confused with - * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. - * - * - */ - -const _ = require('lodash'); -const logger = require('../logger').access; -const validator = require('ajv'); -const error = require('./error'); -const userModel = require('../models/user'); -const proxyHostModel = require('../models/proxy_host'); -const TokenModel = require('../models/token'); -const roleSchema = require('./access/roles.json'); -const permsSchema = require('./access/permissions.json'); - -module.exports = function (token_string) { - let Token = new TokenModel(); - let token_data = null; - let initialised = false; - let object_cache = {}; - let allow_internal_access = false; - let user_roles = []; - let permissions = {}; - - /** - * Loads the Token object from the token string - * - * @returns {Promise} - */ - this.init = () => { - return new Promise((resolve, reject) => { - if (initialised) { - resolve(); - } else if (!token_string) { - reject(new error.PermissionError('Permission Denied')); - } else { - resolve(Token.load(token_string) - .then((data) => { - token_data = data; - - // At this point we need to load the user from the DB and make sure they: - // - exist (and not soft deleted) - // - still have the appropriate scopes for this token - // This is only required when the User ID is supplied or if the token scope has `user` - - if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) { - // Has token user id or token user scope - return userModel - .query() - .where('id', token_data.attrs.id) - .andWhere('is_deleted', 0) - .andWhere('is_disabled', 0) - .allowEager('[permissions]') - .eager('[permissions]') - .first() - .then((user) => { - if (user) { - // make sure user has all scopes of the token - // The `user` role is not added against the user row, so we have to just add it here to get past this check. - user.roles.push('user'); - - let is_ok = true; - _.forEach(token_data.scope, (scope_item) => { - if (_.indexOf(user.roles, scope_item) === -1) { - is_ok = false; - } - }); - - if (!is_ok) { - throw new error.AuthError('Invalid token scope for User'); - } else { - initialised = true; - user_roles = user.roles; - permissions = user.permissions; - } - - } else { - throw new error.AuthError('User cannot be loaded for Token'); - } - }); - } else { - initialised = true; - } - })); - } - }); - }; - - /** - * Fetches the object ids from the database, only once per object type, for this token. - * This only applies to USER token scopes, as all other tokens are not really bound - * by object scopes - * - * @param {String} object_type - * @returns {Promise} - */ - this.loadObjects = (object_type) => { - return new Promise((resolve, reject) => { - if (Token.hasScope('user')) { - if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) { - reject(new error.AuthError('User Token supplied without a User ID')); - } else { - let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0; - let query; - - if (typeof object_cache[object_type] === 'undefined') { - switch (object_type) { - - // USERS - should only return yourself - case 'users': - resolve(token_user_id ? [token_user_id] : []); - break; - - // Proxy Hosts - case 'proxy_hosts': - query = proxyHostModel - .query() - .select('id') - .andWhere('is_deleted', 0); - - if (permissions.visibility === 'user') { - query.andWhere('owner_user_id', token_user_id); - } - - resolve(query - .then((rows) => { - let result = []; - _.forEach(rows, (rule_row) => { - result.push(rule_row.id); - }); - - // enum should not have less than 1 item - if (!result.length) { - result.push(0); - } - - return result; - }) - ); - break; - - // DEFAULT: null - default: - resolve(null); - break; - } - } else { - resolve(object_cache[object_type]); - } - } - } else { - resolve(null); - } - }) - .then((objects) => { - object_cache[object_type] = objects; - return objects; - }); - }; - - /** - * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema - * - * @param {String} permission_label - * @returns {Object} - */ - this.getObjectSchema = (permission_label) => { - let base_object_type = permission_label.split(':').shift(); - - let schema = { - $id: 'objects', - $schema: 'http://json-schema.org/draft-07/schema#', - description: 'Actor Properties', - type: 'object', - additionalProperties: false, - properties: { - user_id: { - anyOf: [ - { - type: 'number', - enum: [Token.get('attrs').id] - } - ] - }, - scope: { - type: 'string', - pattern: '^' + Token.get('scope') + '$' - } - } - }; - - return this.loadObjects(base_object_type) - .then((object_result) => { - if (typeof object_result === 'object' && object_result !== null) { - schema.properties[base_object_type] = { - type: 'number', - enum: object_result, - minimum: 1 - }; - } else { - schema.properties[base_object_type] = { - type: 'number', - minimum: 1 - }; - } - - return schema; - }); - }; - - return { - - token: Token, - - /** - * - * @param {Boolean} [allow_internal] - * @returns {Promise} - */ - load: (allow_internal) => { - return new Promise(function (resolve/*, reject*/) { - if (token_string) { - resolve(Token.load(token_string)); - } else { - allow_internal_access = allow_internal; - resolve(allow_internal_access || null); - } - }); - }, - - reloadObjects: this.loadObjects, - - /** - * - * @param {String} permission - * @param {*} [data] - * @returns {Promise} - */ - can: (permission, data) => { - if (allow_internal_access === true) { - return Promise.resolve(true); - //return true; - } else { - return this.init() - .then(() => { - // Initialised, token decoded ok - return this.getObjectSchema(permission) - .then((objectSchema) => { - let data_schema = { - [permission]: { - data: data, - scope: Token.get('scope'), - roles: user_roles, - permission_visibility: permissions.visibility, - permission_proxy_hosts: permissions.proxy_hosts, - permission_redirection_hosts: permissions.redirection_hosts, - permission_dead_hosts: permissions.dead_hosts, - permission_streams: permissions.streams, - permission_access_lists: permissions.access_lists, - permission_certificates: permissions.certificates - } - }; - - let permissionSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $async: true, - $id: 'permissions', - additionalProperties: false, - properties: {} - }; - - permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json'); - - // logger.info('objectSchema', JSON.stringify(objectSchema, null, 2)); - // logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2)); - // logger.info('data_schema', JSON.stringify(data_schema, null, 2)); - - let ajv = validator({ - verbose: true, - allErrors: true, - format: 'full', - missingRefs: 'fail', - breakOnError: true, - coerceTypes: true, - schemas: [ - roleSchema, - permsSchema, - objectSchema, - permissionSchema - ] - }); - - return ajv.validate('permissions', data_schema) - .then(() => { - return data_schema[permission]; - }); - }); - }) - .catch((err) => { - err.permission = permission; - err.permission_data = data; - logger.error(permission, data, err.message); - - throw new error.PermissionError('Permission Denied', err); - }); - } - } - }; -}; diff --git a/backend/lib/access/access_lists-create.json b/backend/lib/access/access_lists-create.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-delete.json b/backend/lib/access/access_lists-delete.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-get.json b/backend/lib/access/access_lists-get.json deleted file mode 100644 index 8f6dd8cc..00000000 --- a/backend/lib/access/access_lists-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-list.json b/backend/lib/access/access_lists-list.json deleted file mode 100644 index 8f6dd8cc..00000000 --- a/backend/lib/access/access_lists-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/access_lists-update.json b/backend/lib/access/access_lists-update.json deleted file mode 100644 index 5a16a864..00000000 --- a/backend/lib/access/access_lists-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_access_lists", "roles"], - "properties": { - "permission_access_lists": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/auditlog-list.json b/backend/lib/access/auditlog-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/auditlog-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/certificates-create.json b/backend/lib/access/certificates-create.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-delete.json b/backend/lib/access/certificates-delete.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-get.json b/backend/lib/access/certificates-get.json deleted file mode 100644 index 9ccfa4f1..00000000 --- a/backend/lib/access/certificates-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-list.json b/backend/lib/access/certificates-list.json deleted file mode 100644 index 9ccfa4f1..00000000 --- a/backend/lib/access/certificates-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/certificates-update.json b/backend/lib/access/certificates-update.json deleted file mode 100644 index bcdf6674..00000000 --- a/backend/lib/access/certificates-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_certificates", "roles"], - "properties": { - "permission_certificates": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-create.json b/backend/lib/access/dead_hosts-create.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-delete.json b/backend/lib/access/dead_hosts-delete.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-get.json b/backend/lib/access/dead_hosts-get.json deleted file mode 100644 index 87aa12e7..00000000 --- a/backend/lib/access/dead_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-list.json b/backend/lib/access/dead_hosts-list.json deleted file mode 100644 index 87aa12e7..00000000 --- a/backend/lib/access/dead_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/dead_hosts-update.json b/backend/lib/access/dead_hosts-update.json deleted file mode 100644 index a276c681..00000000 --- a/backend/lib/access/dead_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_dead_hosts", "roles"], - "properties": { - "permission_dead_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/permissions.json b/backend/lib/access/permissions.json deleted file mode 100644 index 8480f9a1..00000000 --- a/backend/lib/access/permissions.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "perms", - "definitions": { - "view": { - "type": "string", - "pattern": "^(view|manage)$" - }, - "manage": { - "type": "string", - "pattern": "^(manage)$" - } - } -} diff --git a/backend/lib/access/proxy_hosts-create.json b/backend/lib/access/proxy_hosts-create.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-delete.json b/backend/lib/access/proxy_hosts-delete.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-get.json b/backend/lib/access/proxy_hosts-get.json deleted file mode 100644 index d88e4cff..00000000 --- a/backend/lib/access/proxy_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-list.json b/backend/lib/access/proxy_hosts-list.json deleted file mode 100644 index d88e4cff..00000000 --- a/backend/lib/access/proxy_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/proxy_hosts-update.json b/backend/lib/access/proxy_hosts-update.json deleted file mode 100644 index 166527a3..00000000 --- a/backend/lib/access/proxy_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_proxy_hosts", "roles"], - "properties": { - "permission_proxy_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-create.json b/backend/lib/access/redirection_hosts-create.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-delete.json b/backend/lib/access/redirection_hosts-delete.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-get.json b/backend/lib/access/redirection_hosts-get.json deleted file mode 100644 index ba229206..00000000 --- a/backend/lib/access/redirection_hosts-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-list.json b/backend/lib/access/redirection_hosts-list.json deleted file mode 100644 index ba229206..00000000 --- a/backend/lib/access/redirection_hosts-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/redirection_hosts-update.json b/backend/lib/access/redirection_hosts-update.json deleted file mode 100644 index 342babc8..00000000 --- a/backend/lib/access/redirection_hosts-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_redirection_hosts", "roles"], - "properties": { - "permission_redirection_hosts": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/reports-hosts.json b/backend/lib/access/reports-hosts.json deleted file mode 100644 index dbc9e0c0..00000000 --- a/backend/lib/access/reports-hosts.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/user" - } - ] -} diff --git a/backend/lib/access/roles.json b/backend/lib/access/roles.json deleted file mode 100644 index 16b33b55..00000000 --- a/backend/lib/access/roles.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "roles", - "definitions": { - "admin": { - "type": "object", - "required": ["scope", "roles"], - "properties": { - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - }, - "roles": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^admin$" - } - } - } - }, - "user": { - "type": "object", - "required": ["scope"], - "properties": { - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - } -} diff --git a/backend/lib/access/settings-get.json b/backend/lib/access/settings-get.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-get.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/settings-list.json b/backend/lib/access/settings-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/settings-update.json b/backend/lib/access/settings-update.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/settings-update.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/streams-create.json b/backend/lib/access/streams-create.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-create.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-delete.json b/backend/lib/access/streams-delete.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-get.json b/backend/lib/access/streams-get.json deleted file mode 100644 index 7e996287..00000000 --- a/backend/lib/access/streams-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-list.json b/backend/lib/access/streams-list.json deleted file mode 100644 index 7e996287..00000000 --- a/backend/lib/access/streams-list.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/view" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/streams-update.json b/backend/lib/access/streams-update.json deleted file mode 100644 index fbeb1cc9..00000000 --- a/backend/lib/access/streams-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["permission_streams", "roles"], - "properties": { - "permission_streams": { - "$ref": "perms#/definitions/manage" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "enum": ["user"] - } - } - } - } - ] -} diff --git a/backend/lib/access/users-create.json b/backend/lib/access/users-create.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-create.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-delete.json b/backend/lib/access/users-delete.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-delete.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-get.json b/backend/lib/access/users-get.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-get.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/access/users-list.json b/backend/lib/access/users-list.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-loginas.json b/backend/lib/access/users-loginas.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-loginas.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-password.json b/backend/lib/access/users-password.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-password.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/access/users-permissions.json b/backend/lib/access/users-permissions.json deleted file mode 100644 index aeadc94b..00000000 --- a/backend/lib/access/users-permissions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - } - ] -} diff --git a/backend/lib/access/users-update.json b/backend/lib/access/users-update.json deleted file mode 100644 index 2a2f0423..00000000 --- a/backend/lib/access/users-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "anyOf": [ - { - "$ref": "roles#/definitions/admin" - }, - { - "type": "object", - "required": ["data", "scope"], - "properties": { - "data": { - "$ref": "objects#/properties/users" - }, - "scope": { - "type": "array", - "contains": { - "type": "string", - "pattern": "^user$" - } - } - } - } - ] -} diff --git a/backend/lib/error.js b/backend/lib/error.js deleted file mode 100644 index 9e456f05..00000000 --- a/backend/lib/error.js +++ /dev/null @@ -1,90 +0,0 @@ -const _ = require('lodash'); -const util = require('util'); - -module.exports = { - - PermissionError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = 'Permission Denied'; - this.public = true; - this.status = 403; - }, - - ItemNotFoundError: function (id, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = 'Item Not Found - ' + id; - this.public = true; - this.status = 404; - }, - - AuthError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = true; - this.status = 401; - }, - - InternalError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 500; - this.public = false; - }, - - InternalValidationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 400; - this.public = false; - }, - - ConfigurationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.status = 400; - this.public = true; - }, - - CacheError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; - this.previous = previous; - this.status = 500; - this.public = false; - }, - - ValidationError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = true; - this.status = 400; - }, - - AssertionFailedError: function (message, previous) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.previous = previous; - this.message = message; - this.public = false; - this.status = 400; - } -}; - -_.forEach(module.exports, function (error) { - util.inherits(error, Error); -}); diff --git a/backend/lib/express/cors.js b/backend/lib/express/cors.js deleted file mode 100644 index c9befeec..00000000 --- a/backend/lib/express/cors.js +++ /dev/null @@ -1,40 +0,0 @@ -const validator = require('../validator'); - -module.exports = function (req, res, next) { - - if (req.headers.origin) { - - const originSchema = { - oneOf: [ - { - type: 'string', - pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$' - }, - { - type: 'string', - pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$' - } - ] - }; - - // very relaxed validation.... - validator(originSchema, req.headers.origin) - .then(function () { - res.set({ - 'Access-Control-Allow-Origin': req.headers.origin, - 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', - 'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit', - 'Access-Control-Max-Age': 5 * 60, - 'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit' - }); - next(); - }) - .catch(next); - - } else { - // No origin - next(); - } - -}; diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js deleted file mode 100644 index 17edccec..00000000 --- a/backend/lib/express/jwt-decode.js +++ /dev/null @@ -1,15 +0,0 @@ -const Access = require('../access'); - -module.exports = () => { - return function (req, res, next) { - res.locals.access = null; - let access = new Access(res.locals.token || null); - access.load() - .then(() => { - res.locals.access = access; - next(); - }) - .catch(next); - }; -}; - diff --git a/backend/lib/express/jwt.js b/backend/lib/express/jwt.js deleted file mode 100644 index 44aa3693..00000000 --- a/backend/lib/express/jwt.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = function () { - return function (req, res, next) { - if (req.headers.authorization) { - let parts = req.headers.authorization.split(' '); - - if (parts && parts[0] === 'Bearer' && parts[1]) { - res.locals.token = parts[1]; - } - } - - next(); - }; -}; diff --git a/backend/lib/express/pagination.js b/backend/lib/express/pagination.js deleted file mode 100644 index 24ffa58d..00000000 --- a/backend/lib/express/pagination.js +++ /dev/null @@ -1,55 +0,0 @@ -let _ = require('lodash'); - -module.exports = function (default_sort, default_offset, default_limit, max_limit) { - - /** - * This will setup the req query params with filtered data and defaults - * - * sort will be an array of fields and their direction - * offset will be an int, defaulting to zero if no other default supplied - * limit will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied - * - */ - - return function (req, res, next) { - - req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10); - req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10); - - if (max_limit && req.query.limit > max_limit) { - req.query.limit = max_limit; - } - - // Sorting - let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort; - let myRegexp = /.*\.(asc|desc)$/ig; - let sort_array = []; - - sort = sort.split(','); - _.map(sort, function (val) { - let matches = myRegexp.exec(val); - - if (matches !== null) { - let dir = matches[1]; - sort_array.push({ - field: val.substr(0, val.length - (dir.length + 1)), - dir: dir.toLowerCase() - }); - } else { - sort_array.push({ - field: val, - dir: 'asc' - }); - } - }); - - // Sort will now be in this format: - // [ - // { field: 'field1', dir: 'asc' }, - // { field: 'field2', dir: 'desc' } - // ] - - req.query.sort = sort_array; - next(); - }; -}; diff --git a/backend/lib/express/user-id-from-me.js b/backend/lib/express/user-id-from-me.js deleted file mode 100644 index 4a37a406..00000000 --- a/backend/lib/express/user-id-from-me.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = (req, res, next) => { - if (req.params.user_id === 'me' && res.locals.access) { - req.params.user_id = res.locals.access.token.get('attrs').id; - } else { - req.params.user_id = parseInt(req.params.user_id, 10); - } - - next(); -}; diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js deleted file mode 100644 index e38be991..00000000 --- a/backend/lib/helpers.js +++ /dev/null @@ -1,32 +0,0 @@ -const moment = require('moment'); - -module.exports = { - - /** - * Takes an expression such as 30d and returns a moment object of that date in future - * - * Key Shorthand - * ================== - * years y - * quarters Q - * months M - * weeks w - * days d - * hours h - * minutes m - * seconds s - * milliseconds ms - * - * @param {String} expression - * @returns {Object} - */ - parseDatePeriod: function (expression) { - let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m); - if (matches) { - return moment().add(matches[1], matches[2]); - } - - return null; - } - -}; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js deleted file mode 100644 index f75f77ef..00000000 --- a/backend/lib/migrate_template.js +++ /dev/null @@ -1,55 +0,0 @@ -const migrate_name = 'identifier_for_migrate'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex, Promise) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - // Create Table example: - - /*return knex.schema.createTable('notification', (table) => { - table.increments().primary(); - table.string('name').notNull(); - table.string('type').notNull(); - table.integer('created_on').notNull(); - table.integer('modified_on').notNull(); - }) - .then(function () { - logger.info('[' + migrate_name + '] Notification Table created'); - });*/ - - logger.info('[' + migrate_name + '] Migrating Up Complete'); - - return Promise.resolve(true); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - // Drop table example: - - /*return knex.schema.dropTable('notification') - .then(() => { - logger.info('[' + migrate_name + '] Notification Table dropped'); - });*/ - - logger.info('[' + migrate_name + '] Migrating Down Complete'); - - return Promise.resolve(true); -}; diff --git a/backend/lib/utils.js b/backend/lib/utils.js deleted file mode 100644 index 4c8b62a8..00000000 --- a/backend/lib/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -const exec = require('child_process').exec; - -module.exports = { - - /** - * @param {String} cmd - * @returns {Promise} - */ - exec: function (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, function (err, stdout, /*stderr*/) { - if (err && typeof err === 'object') { - reject(err); - } else { - resolve(stdout.trim()); - } - }); - }); - } -}; diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js deleted file mode 100644 index 3f51b596..00000000 --- a/backend/lib/validator/api.js +++ /dev/null @@ -1,45 +0,0 @@ -const error = require('../error'); -const path = require('path'); -const parser = require('json-schema-ref-parser'); - -const ajv = require('ajv')({ - verbose: true, - validateSchema: true, - allErrors: false, - format: 'full', - coerceTypes: true -}); - -/** - * @param {Object} schema - * @param {Object} payload - * @returns {Promise} - */ -function apiValidator (schema, payload/*, description*/) { - return new Promise(function Promise_apiValidator (resolve, reject) { - if (typeof payload === 'undefined') { - reject(new error.ValidationError('Payload is undefined')); - } - - let validate = ajv.compile(schema); - let valid = validate(payload); - - if (valid && !validate.errors) { - resolve(payload); - } else { - let message = ajv.errorsText(validate.errors); - let err = new error.ValidationError(message); - err.debug = [validate.errors, payload]; - reject(err); - } - }); -} - -apiValidator.loadSchemas = parser - .dereference(path.resolve('schema/index.json')) - .then((schema) => { - ajv.addSchema(schema); - return schema; - }); - -module.exports = apiValidator; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js deleted file mode 100644 index fca6f4bf..00000000 --- a/backend/lib/validator/index.js +++ /dev/null @@ -1,49 +0,0 @@ -const _ = require('lodash'); -const error = require('../error'); -const definitions = require('../../schema/definitions.json'); - -RegExp.prototype.toJSON = RegExp.prototype.toString; - -const ajv = require('ajv')({ - verbose: true, //process.env.NODE_ENV === 'development', - allErrors: true, - format: 'full', // strict regexes for format checks - coerceTypes: true, - schemas: [ - definitions - ] -}); - -/** - * - * @param {Object} schema - * @param {Object} payload - * @returns {Promise} - */ -function validator (schema, payload) { - return new Promise(function (resolve, reject) { - if (!payload) { - reject(new error.InternalValidationError('Payload is falsy')); - } else { - try { - let validate = ajv.compile(schema); - - let valid = validate(payload); - if (valid && !validate.errors) { - resolve(_.cloneDeep(payload)); - } else { - let message = ajv.errorsText(validate.errors); - reject(new error.InternalValidationError(message)); - } - - } catch (err) { - reject(err); - } - - } - - }); - -} - -module.exports = validator; diff --git a/backend/logger.js b/backend/logger.js deleted file mode 100644 index 680af6d5..00000000 --- a/backend/logger.js +++ /dev/null @@ -1,13 +0,0 @@ -const {Signale} = require('signale'); - -module.exports = { - global: new Signale({scope: 'Global '}), - migrate: new Signale({scope: 'Migrate '}), - express: new Signale({scope: 'Express '}), - access: new Signale({scope: 'Access '}), - nginx: new Signale({scope: 'Nginx '}), - ssl: new Signale({scope: 'SSL '}), - import: new Signale({scope: 'Importer '}), - setup: new Signale({scope: 'Setup '}), - ip_ranges: new Signale({scope: 'IP Ranges'}) -}; diff --git a/backend/migrate.js b/backend/migrate.js deleted file mode 100644 index 263c8702..00000000 --- a/backend/migrate.js +++ /dev/null @@ -1,15 +0,0 @@ -const db = require('./db'); -const logger = require('./logger').migrate; - -module.exports = { - latest: function () { - return db.migrate.currentVersion() - .then((version) => { - logger.info('Current database version:', version); - return db.migrate.latest({ - tableName: 'migrations', - directory: 'migrations' - }); - }); - } -}; diff --git a/backend/migrations/20180618015850_initial.js b/backend/migrations/20180618015850_initial.js deleted file mode 100644 index a112e826..00000000 --- a/backend/migrations/20180618015850_initial.js +++ /dev/null @@ -1,205 +0,0 @@ -const migrate_name = 'initial-schema'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('auth', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('type', 30).notNull(); - table.string('secret').notNull(); - table.json('meta').notNull(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] auth Table created'); - - return knex.schema.createTable('user', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.integer('is_disabled').notNull().unsigned().defaultTo(0); - table.string('email').notNull(); - table.string('name').notNull(); - table.string('nickname').notNull(); - table.string('avatar').notNull(); - table.json('roles').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] user Table created'); - - return knex.schema.createTable('user_permission', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('visibility').notNull(); - table.string('proxy_hosts').notNull(); - table.string('redirection_hosts').notNull(); - table.string('dead_hosts').notNull(); - table.string('streams').notNull(); - table.string('access_lists').notNull(); - table.string('certificates').notNull(); - table.unique('user_id'); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] user_permission Table created'); - - return knex.schema.createTable('proxy_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.string('forward_ip').notNull(); - table.integer('forward_port').notNull().unsigned(); - table.integer('access_list_id').notNull().unsigned().defaultTo(0); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.integer('caching_enabled').notNull().unsigned().defaultTo(0); - table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table created'); - - return knex.schema.createTable('redirection_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.string('forward_domain_name').notNull(); - table.integer('preserve_path').notNull().unsigned().defaultTo(0); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.integer('block_exploits').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table created'); - - return knex.schema.createTable('dead_host', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.json('domain_names').notNull(); - table.integer('certificate_id').notNull().unsigned().defaultTo(0); - table.integer('ssl_forced').notNull().unsigned().defaultTo(0); - table.text('advanced_config').notNull().defaultTo(''); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table created'); - - return knex.schema.createTable('stream', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.integer('incoming_port').notNull().unsigned(); - table.string('forward_ip').notNull(); - table.integer('forwarding_port').notNull().unsigned(); - table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); - table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] stream Table created'); - - return knex.schema.createTable('access_list', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('name').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table created'); - - return knex.schema.createTable('certificate', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('owner_user_id').notNull().unsigned(); - table.integer('is_deleted').notNull().unsigned().defaultTo(0); - table.string('provider').notNull(); - table.string('nice_name').notNull().defaultTo(''); - table.json('domain_names').notNull(); - table.dateTime('expires_on').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] certificate Table created'); - - return knex.schema.createTable('access_list_auth', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('access_list_id').notNull().unsigned(); - table.string('username').notNull(); - table.string('password').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list_auth Table created'); - - return knex.schema.createTable('audit_log', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('user_id').notNull().unsigned(); - table.string('object_type').notNull().defaultTo(''); - table.integer('object_id').notNull().unsigned().defaultTo(0); - table.string('action').notNull(); - table.json('meta').notNull(); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] audit_log Table created'); - }); - -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20180929054513_websockets.js b/backend/migrations/20180929054513_websockets.js deleted file mode 100644 index 06054850..00000000 --- a/backend/migrations/20180929054513_websockets.js +++ /dev/null @@ -1,35 +0,0 @@ -const migrate_name = 'websockets'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); - -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; \ No newline at end of file diff --git a/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js deleted file mode 100644 index 05c27739..00000000 --- a/backend/migrations/20181019052346_forward_host.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'forward_host'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.renameColumn('forward_ip', 'forward_host'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; \ No newline at end of file diff --git a/backend/migrations/20181113041458_http2_support.js b/backend/migrations/20181113041458_http2_support.js deleted file mode 100644 index 9f6b4336..00000000 --- a/backend/migrations/20181113041458_http2_support.js +++ /dev/null @@ -1,49 +0,0 @@ -const migrate_name = 'http2_support'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('http2_support').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; - diff --git a/backend/migrations/20181213013211_forward_scheme.js b/backend/migrations/20181213013211_forward_scheme.js deleted file mode 100644 index 22ae619e..00000000 --- a/backend/migrations/20181213013211_forward_scheme.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'forward_scheme'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.string('forward_scheme').notNull().defaultTo('http'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190104035154_disabled.js b/backend/migrations/20190104035154_disabled.js deleted file mode 100644 index 2780c4df..00000000 --- a/backend/migrations/20190104035154_disabled.js +++ /dev/null @@ -1,55 +0,0 @@ -const migrate_name = 'disabled'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('enabled').notNull().unsigned().defaultTo(1); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - - return knex.schema.table('stream', function (stream) { - stream.integer('enabled').notNull().unsigned().defaultTo(1); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] stream Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190215115310_customlocations.js b/backend/migrations/20190215115310_customlocations.js deleted file mode 100644 index 4bcfd51a..00000000 --- a/backend/migrations/20190215115310_customlocations.js +++ /dev/null @@ -1,35 +0,0 @@ -const migrate_name = 'custom_locations'; -const logger = require('../logger').migrate; - -/** - * Migrate - * Extends proxy_host table with locations field - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.json('locations'); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190218060101_hsts.js b/backend/migrations/20190218060101_hsts.js deleted file mode 100644 index 648b162a..00000000 --- a/backend/migrations/20190218060101_hsts.js +++ /dev/null @@ -1,51 +0,0 @@ -const migrate_name = 'hsts'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('proxy_host', function (proxy_host) { - proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - - return knex.schema.table('redirection_host', function (redirection_host) { - redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - - return knex.schema.table('dead_host', function (dead_host) { - dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0); - dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] dead_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20190227065017_settings.js b/backend/migrations/20190227065017_settings.js deleted file mode 100644 index 7dc9c192..00000000 --- a/backend/migrations/20190227065017_settings.js +++ /dev/null @@ -1,38 +0,0 @@ -const migrate_name = 'settings'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('setting', (table) => { - table.string('id').notNull().primary(); - table.string('name', 100).notNull(); - table.string('description', 255).notNull(); - table.string('value', 255).notNull(); - table.json('meta').notNull(); - }) - .then(() => { - logger.info('[' + migrate_name + '] setting Table created'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20200410143839_access_list_client.js b/backend/migrations/20200410143839_access_list_client.js deleted file mode 100644 index 3511e35b..00000000 --- a/backend/migrations/20200410143839_access_list_client.js +++ /dev/null @@ -1,53 +0,0 @@ -const migrate_name = 'access_list_client'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.createTable('access_list_client', (table) => { - table.increments().primary(); - table.dateTime('created_on').notNull(); - table.dateTime('modified_on').notNull(); - table.integer('access_list_id').notNull().unsigned(); - table.string('address').notNull(); - table.string('directive').notNull(); - table.json('meta').notNull(); - - }) - .then(function () { - logger.info('[' + migrate_name + '] access_list_client Table created'); - - return knex.schema.table('access_list', function (access_list) { - access_list.integer('satify_any').notNull().defaultTo(0); - }); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.dropTable('access_list_client') - .then(() => { - logger.info('[' + migrate_name + '] access_list_client Table dropped'); - }); -}; diff --git a/backend/migrations/20200410143840_access_list_client_fix.js b/backend/migrations/20200410143840_access_list_client_fix.js deleted file mode 100644 index ee0f0906..00000000 --- a/backend/migrations/20200410143840_access_list_client_fix.js +++ /dev/null @@ -1,34 +0,0 @@ -const migrate_name = 'access_list_client_fix'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.renameColumn('satify_any', 'satisfy_any'); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/migrations/20201013035318_initial_schema.sql b/backend/migrations/20201013035318_initial_schema.sql new file mode 100644 index 00000000..aa65b2ab --- /dev/null +++ b/backend/migrations/20201013035318_initial_schema.sql @@ -0,0 +1,172 @@ +-- migrate:up + +CREATE TABLE IF NOT EXISTS `user` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + nickname TEXT NOT NULL, + email TEXT NOT NULL, + roles TEXT NOT NULL, + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `auth` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + secret TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + UNIQUE (user_id, type) +); + +CREATE TABLE IF NOT EXISTS `setting` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS `audit_log` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + object_type TEXT NOT NULL, + object_id INTEGER NOT NULL, + action TEXT NOT NULL, + meta TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `certificate_authority` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + acme2_url TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `dns_provider` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + provider_key TEXT NOT NULL, + name TEXT NOT NULL, + meta TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `certificate` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + type TEXT NOT NULL, -- custom,dns,http + user_id INTEGER NOT NULL, + certificate_authority_id INTEGER, -- 0 for a custom cert + dns_provider_id INTEGER, -- 0, for a http or custom cert + name TEXT NOT NULL, + domain_names TEXT NOT NULL, + expires_on INTEGER DEFAULT 0, + status TEXT NOT NULL, -- ready,requesting,failed,provided + error_message text NOT NULL DEFAULT "", + meta TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + FOREIGN KEY (certificate_authority_id) REFERENCES certificate_authority (id), + FOREIGN KEY (dns_provider_id) REFERENCES dns_provider (id) +); + +CREATE TABLE IF NOT EXISTS `stream` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + listen_interface TEXT NOT NULL, + incoming_port INTEGER NOT NULL, + upstream_options TEXT NOT NULL, + tcp_forwarding INTEGER NOT NULL DEFAULT 0, + udp_forwarding INTEGER NOT NULL DEFAULT 0, + advanced_config TEXT NOT NULL, + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `upstream` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + hosts TEXT NOT NULL, + balance_method TEXT NOT NULL, + max_fails INTEGER NOT NULL DEFAULT 1, + fail_timeout INTEGER NOT NULL DEFAULT 10, + advanced_config TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `access_list` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + meta TEXT NOT NULL, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE IF NOT EXISTS `host` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + listen_interface TEXT NOT NULL, + domain_names TEXT NOT NULL, + upstream_id INTEGER NOT NULL, + certificate_id INTEGER, + access_list_id INTEGER, + ssl_forced INTEGER NOT NULL DEFAULT 0, + caching_enabled INTEGER NOT NULL DEFAULT 0, + block_exploits INTEGER NOT NULL DEFAULT 0, + allow_websocket_upgrade INTEGER NOT NULL DEFAULT 0, + http2_support INTEGER NOT NULL DEFAULT 0, + hsts_enabled INTEGER NOT NULL DEFAULT 0, + hsts_subdomains INTEGER NOT NULL DEFAULT 0, + paths TEXT NOT NULL, + upstream_options TEXT NOT NULL DEFAULT "", + advanced_config TEXT NOT NULL DEFAULT "", + is_disabled INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES user (id), + FOREIGN KEY (upstream_id) REFERENCES upstream (id), + FOREIGN KEY (certificate_id) REFERENCES certificate (id), + FOREIGN KEY (access_list_id) REFERENCES access_list (id) +); + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/migrations/20201013035839_initial_data.sql b/backend/migrations/20201013035839_initial_data.sql new file mode 100644 index 00000000..dad2ac8d --- /dev/null +++ b/backend/migrations/20201013035839_initial_data.sql @@ -0,0 +1,38 @@ +-- migrate:up + +-- Default error reporting setting +INSERT INTO `setting` ( + created_on, + modified_on, + name, + value +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "error-reporting", + "true" +); + +-- Default Certificate Authorities + +INSERT INTO `certificate_authority` ( + created_on, + modified_on, + name, + acme2_url +) VALUES ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Let's Encrypt", + "https://acme-v02.api.letsencrypt.org/directory" +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + "Let's Encrypt (Staging)", + "https://acme-staging-v02.api.letsencrypt.org/directory" +); + + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/migrations/20201014143841_pass_auth.js b/backend/migrations/20201014143841_pass_auth.js deleted file mode 100644 index a7767eb1..00000000 --- a/backend/migrations/20201014143841_pass_auth.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'pass_auth'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.integer('pass_auth').notNull().defaultTo(1); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('access_list', function (access_list) { - access_list.dropColumn('pass_auth'); - }) - .then(() => { - logger.info('[' + migrate_name + '] access_list pass_auth Column dropped'); - }); -}; diff --git a/backend/migrations/20210210154702_redirection_scheme.js b/backend/migrations/20210210154702_redirection_scheme.js deleted file mode 100644 index 0dad4876..00000000 --- a/backend/migrations/20210210154702_redirection_scheme.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'redirection_scheme'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('redirection_host', (table) => { - table.string('forward_scheme').notNull().defaultTo('$scheme'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('redirection_host', (table) => { - table.dropColumn('forward_scheme'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; diff --git a/backend/migrations/20210210154703_redirection_status_code.js b/backend/migrations/20210210154703_redirection_status_code.js deleted file mode 100644 index b9bea0b9..00000000 --- a/backend/migrations/20210210154703_redirection_status_code.js +++ /dev/null @@ -1,41 +0,0 @@ -const migrate_name = 'redirection_status_code'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.table('redirection_host', (table) => { - table.integer('forward_http_code').notNull().unsigned().defaultTo(302); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return knex.schema.table('redirection_host', (table) => { - table.dropColumn('forward_http_code'); - }) - .then(function () { - logger.info('[' + migrate_name + '] redirection_host Table altered'); - }); -}; diff --git a/backend/models/access_list.js b/backend/models/access_list.js deleted file mode 100644 index 01974e86..00000000 --- a/backend/models/access_list.js +++ /dev/null @@ -1,102 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessListAuth = require('./access_list_auth'); -const AccessListClient = require('./access_list_client'); -const now = require('./now_helper'); - -Model.knex(db); - -class AccessList extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessList'; - } - - static get tableName () { - return 'access_list'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - const ProxyHost = require('./proxy_host'); - - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'access_list.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - items: { - relation: Model.HasManyRelation, - modelClass: AccessListAuth, - join: { - from: 'access_list.id', - to: 'access_list_auth.access_list_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); - } - }, - clients: { - relation: Model.HasManyRelation, - modelClass: AccessListClient, - join: { - from: 'access_list.id', - to: 'access_list_client.access_list_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']); - } - }, - proxy_hosts: { - relation: Model.HasManyRelation, - modelClass: ProxyHost, - join: { - from: 'access_list.id', - to: 'proxy_host.access_list_id' - }, - modify: function (qb) { - qb.where('proxy_host.is_deleted', 0); - qb.omit(['is_deleted', 'meta']); - } - } - }; - } - - get satisfy() { - return this.satisfy_any ? 'satisfy any' : 'satisfy all'; - } - - get passauth() { - return this.pass_auth ? '' : 'proxy_set_header Authorization "";'; - } -} - -module.exports = AccessList; diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js deleted file mode 100644 index 932371f3..00000000 --- a/backend/models/access_list_auth.js +++ /dev/null @@ -1,55 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class AccessListAuth extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessListAuth'; - } - - static get tableName () { - return 'access_list_auth'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - access_list: { - relation: Model.HasOneRelation, - modelClass: require('./access_list'), - join: { - from: 'access_list_auth.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); - } - } - }; - } -} - -module.exports = AccessListAuth; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js deleted file mode 100644 index e257213a..00000000 --- a/backend/models/access_list_client.js +++ /dev/null @@ -1,59 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class AccessListClient extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AccessListClient'; - } - - static get tableName () { - return 'access_list_client'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - access_list: { - relation: Model.HasOneRelation, - modelClass: require('./access_list'), - join: { - from: 'access_list_client.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']); - } - } - }; - } - - get rule() { - return `${this.directive} ${this.address}`; - } -} - -module.exports = AccessListClient; diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js deleted file mode 100644 index a3a318c8..00000000 --- a/backend/models/audit-log.js +++ /dev/null @@ -1,55 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class AuditLog extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'AuditLog'; - } - - static get tableName () { - return 'audit_log'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - user: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'audit_log.user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'roles']); - } - } - }; - } -} - -module.exports = AuditLog; diff --git a/backend/models/auth.js b/backend/models/auth.js deleted file mode 100644 index 5ba5f380..00000000 --- a/backend/models/auth.js +++ /dev/null @@ -1,86 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const bcrypt = require('bcrypt'); -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -function encryptPassword () { - /* jshint -W040 */ - let _this = this; - - if (_this.type === 'password' && _this.secret) { - return bcrypt.hash(_this.secret, 13) - .then(function (hash) { - _this.secret = hash; - }); - } - - return null; -} - -class Auth extends Model { - $beforeInsert (queryContext) { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - return encryptPassword.apply(this, queryContext); - } - - $beforeUpdate (queryContext) { - this.modified_on = now(); - return encryptPassword.apply(this, queryContext); - } - - /** - * Verify a plain password against the encrypted password - * - * @param {String} password - * @returns {Promise} - */ - verifyPassword (password) { - return bcrypt.compare(password, this.secret); - } - - static get name () { - return 'Auth'; - } - - static get tableName () { - return 'auth'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - user: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'auth.user_id', - to: 'user.id' - }, - filter: { - is_deleted: 0 - }, - modify: function (qb) { - qb.omit(['is_deleted']); - } - } - }; - } -} - -module.exports = Auth; diff --git a/backend/models/certificate.js b/backend/models/certificate.js deleted file mode 100644 index 6084a995..00000000 --- a/backend/models/certificate.js +++ /dev/null @@ -1,73 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class Certificate extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for expires_on - if (typeof this.expires_on === 'undefined') { - this.expires_on = now(); - } - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'Certificate'; - } - - static get tableName () { - return 'certificate'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'certificate.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - } - }; - } -} - -module.exports = Certificate; diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js deleted file mode 100644 index 6de42a33..00000000 --- a/backend/models/dead_host.js +++ /dev/null @@ -1,81 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class DeadHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'DeadHost'; - } - - static get tableName () { - return 'dead_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'dead_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'dead_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = DeadHost; diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js deleted file mode 100644 index def16d08..00000000 --- a/backend/models/now_helper.js +++ /dev/null @@ -1,13 +0,0 @@ -const db = require('../db'); -const config = require('config'); -const Model = require('objection').Model; - -Model.knex(db); - -module.exports = function () { - if (config.database.knex && config.database.knex.client === 'sqlite3') { - return Model.raw('datetime(\'now\',\'localtime\')'); - } else { - return Model.raw('NOW()'); - } -}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js deleted file mode 100644 index a7583088..00000000 --- a/backend/models/proxy_host.js +++ /dev/null @@ -1,94 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const AccessList = require('./access_list'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class ProxyHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'ProxyHost'; - } - - static get tableName () { - return 'proxy_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta', 'locations']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'proxy_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - access_list: { - relation: Model.HasOneRelation, - modelClass: AccessList, - join: { - from: 'proxy_host.access_list_id', - to: 'access_list.id' - }, - modify: function (qb) { - qb.where('access_list.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'proxy_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = ProxyHost; diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js deleted file mode 100644 index dd149b76..00000000 --- a/backend/models/redirection_host.js +++ /dev/null @@ -1,81 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const Certificate = require('./certificate'); -const now = require('./now_helper'); - -Model.knex(db); - -class RedirectionHost extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === 'undefined') { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate () { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== 'undefined') { - this.domain_names.sort(); - } - } - - static get name () { - return 'RedirectionHost'; - } - - static get tableName () { - return 'redirection_host'; - } - - static get jsonAttributes () { - return ['domain_names', 'meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'redirection_host.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: 'redirection_host.certificate_id', - to: 'certificate.id' - }, - modify: function (qb) { - qb.where('certificate.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']); - } - } - }; - } -} - -module.exports = RedirectionHost; diff --git a/backend/models/setting.js b/backend/models/setting.js deleted file mode 100644 index 75aa9007..00000000 --- a/backend/models/setting.js +++ /dev/null @@ -1,30 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; - -Model.knex(db); - -class Setting extends Model { - $beforeInsert () { - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - static get name () { - return 'Setting'; - } - - static get tableName () { - return 'setting'; - } - - static get jsonAttributes () { - return ['meta']; - } -} - -module.exports = Setting; diff --git a/backend/models/stream.js b/backend/models/stream.js deleted file mode 100644 index ed65de0f..00000000 --- a/backend/models/stream.js +++ /dev/null @@ -1,56 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const User = require('./user'); -const now = require('./now_helper'); - -Model.knex(db); - -class Stream extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for meta - if (typeof this.meta === 'undefined') { - this.meta = {}; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'Stream'; - } - - static get tableName () { - return 'stream'; - } - - static get jsonAttributes () { - return ['meta']; - } - - static get relationMappings () { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: 'stream.owner_user_id', - to: 'user.id' - }, - modify: function (qb) { - qb.where('user.is_deleted', 0); - qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']); - } - } - }; - } -} - -module.exports = Stream; diff --git a/backend/models/token.js b/backend/models/token.js deleted file mode 100644 index 4e1b1826..00000000 --- a/backend/models/token.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - NOTE: This is not a database table, this is a model of a Token object that can be created/loaded - and then has abilities after that. - */ - -const _ = require('lodash'); -const jwt = require('jsonwebtoken'); -const crypto = require('crypto'); -const error = require('../lib/error'); -const ALGO = 'RS256'; - -let public_key = null; -let private_key = null; - -function checkJWTKeyPair() { - if (!public_key || !private_key) { - let config = require('config'); - public_key = config.get('jwt.pub'); - private_key = config.get('jwt.key'); - } -} - -module.exports = function () { - - let token_data = {}; - - let self = { - /** - * @param {Object} payload - * @returns {Promise} - */ - create: (payload) => { - // sign with RSA SHA256 - let options = { - algorithm: ALGO, - expiresIn: payload.expiresIn || '1d' - }; - - payload.jti = crypto.randomBytes(12) - .toString('base64') - .substr(-8); - - checkJWTKeyPair(); - - return new Promise((resolve, reject) => { - jwt.sign(payload, private_key, options, (err, token) => { - if (err) { - reject(err); - } else { - token_data = payload; - resolve({ - token: token, - payload: payload - }); - } - }); - }); - }, - - /** - * @param {String} token - * @returns {Promise} - */ - load: function (token) { - return new Promise((resolve, reject) => { - checkJWTKeyPair(); - try { - if (!token || token === null || token === 'null') { - reject(new error.AuthError('Empty token')); - } else { - jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => { - if (err) { - - if (err.name === 'TokenExpiredError') { - reject(new error.AuthError('Token has expired', err)); - } else { - reject(err); - } - - } else { - token_data = result; - - // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'. - // For 30 days at least, we need to replace 'all' with user. - if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) { - //console.log('Warning! Replacing "all" scope with "user"'); - - token_data.scope = ['user']; - } - - resolve(token_data); - } - }); - } - } catch (err) { - reject(err); - } - }); - - }, - - /** - * Does the token have the specified scope? - * - * @param {String} scope - * @returns {Boolean} - */ - hasScope: function (scope) { - return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1; - }, - - /** - * @param {String} key - * @return {*} - */ - get: function (key) { - if (typeof token_data[key] !== 'undefined') { - return token_data[key]; - } - - return null; - }, - - /** - * @param {String} key - * @param {*} value - */ - set: function (key, value) { - token_data[key] = value; - }, - - /** - * @param [default_value] - * @returns {Integer} - */ - getUserId: (default_value) => { - let attrs = self.get('attrs'); - if (attrs && typeof attrs.id !== 'undefined' && attrs.id) { - return attrs.id; - } - - return default_value || 0; - } - }; - - return self; -}; diff --git a/backend/models/user.js b/backend/models/user.js deleted file mode 100644 index c76f7dbf..00000000 --- a/backend/models/user.js +++ /dev/null @@ -1,56 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const UserPermission = require('./user_permission'); -const now = require('./now_helper'); - -Model.knex(db); - -class User extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - - // Default for roles - if (typeof this.roles === 'undefined') { - this.roles = []; - } - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'User'; - } - - static get tableName () { - return 'user'; - } - - static get jsonAttributes () { - return ['roles']; - } - - static get relationMappings () { - return { - permissions: { - relation: Model.HasOneRelation, - modelClass: UserPermission, - join: { - from: 'user.id', - to: 'user_permission.user_id' - }, - modify: function (qb) { - qb.omit(['id', 'created_on', 'modified_on', 'user_id']); - } - } - }; - } - -} - -module.exports = User; diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js deleted file mode 100644 index bb87d5dc..00000000 --- a/backend/models/user_permission.js +++ /dev/null @@ -1,29 +0,0 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -const db = require('../db'); -const Model = require('objection').Model; -const now = require('./now_helper'); - -Model.knex(db); - -class UserPermission extends Model { - $beforeInsert () { - this.created_on = now(); - this.modified_on = now(); - } - - $beforeUpdate () { - this.modified_on = now(); - } - - static get name () { - return 'UserPermission'; - } - - static get tableName () { - return 'user_permission'; - } -} - -module.exports = UserPermission; diff --git a/backend/nodemon.json b/backend/nodemon.json deleted file mode 100644 index 3d6d1342..00000000 --- a/backend/nodemon.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "verbose": false, - "ignore": [ - "data" - ], - "ext": "js json ejs" -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 2130c7b8..00000000 --- a/backend/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "nginx-proxy-manager", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "js/index.js", - "dependencies": { - "ajv": "^6.12.0", - "batchflow": "^0.4.0", - "bcrypt": "^5.0.0", - "body-parser": "^1.19.0", - "compression": "^1.7.4", - "config": "^3.3.1", - "diskdb": "^0.1.17", - "express": "^4.17.1", - "express-fileupload": "^1.1.9", - "gravatar": "^1.8.0", - "html-entities": "^1.2.1", - "json-schema-ref-parser": "^8.0.0", - "jsonwebtoken": "^8.5.1", - "knex": "^0.20.13", - "liquidjs": "^9.11.10", - "lodash": "^4.17.21", - "moment": "^2.24.0", - "mysql": "^2.18.1", - "node-rsa": "^1.0.8", - "nodemon": "^2.0.2", - "objection": "^2.1.3", - "path": "^0.12.7", - "pg": "^7.12.1", - "restler": "^3.4.0", - "signale": "^1.4.0", - "sqlite3": "^4.1.1", - "temp-write": "^4.0.0", - "unix-timestamp": "^0.2.0" - }, - "signale": { - "displayDate": true, - "displayTimestamp": true - }, - "author": "Jamie Curnow ", - "license": "MIT", - "devDependencies": { - "eslint": "^6.8.0", - "eslint-plugin-align-assignments": "^1.1.2", - "prettier": "^2.0.4" - } -} diff --git a/backend/routes/api/audit-log.js b/backend/routes/api/audit-log.js deleted file mode 100644 index 8a2490c3..00000000 --- a/backend/routes/api/audit-log.js +++ /dev/null @@ -1,52 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalAuditLog = require('../../internal/audit-log'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/audit-log - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/audit-log - * - * Retrieve all logs - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalAuditLog.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js deleted file mode 100644 index 33cbbc21..00000000 --- a/backend/routes/api/main.js +++ /dev/null @@ -1,51 +0,0 @@ -const express = require('express'); -const pjson = require('../../package.json'); -const error = require('../../lib/error'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * Health Check - * GET /api - */ -router.get('/', (req, res/*, next*/) => { - let version = pjson.version.split('-').shift().split('.'); - - res.status(200).send({ - status: 'OK', - version: { - major: parseInt(version.shift(), 10), - minor: parseInt(version.shift(), 10), - revision: parseInt(version.shift(), 10) - } - }); -}); - -router.use('/schema', require('./schema')); -router.use('/tokens', require('./tokens')); -router.use('/users', require('./users')); -router.use('/audit-log', require('./audit-log')); -router.use('/reports', require('./reports')); -router.use('/settings', require('./settings')); -router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); -router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); -router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); -router.use('/nginx/streams', require('./nginx/streams')); -router.use('/nginx/access-lists', require('./nginx/access_lists')); -router.use('/nginx/certificates', require('./nginx/certificates')); - -/** - * API 404 for all other routes - * - * ALL /api/* - */ -router.all(/(.+)/, function (req, res, next) { - req.params.page = req.params['0']; - next(new error.ItemNotFoundError(req.params.page)); -}); - -module.exports = router; diff --git a/backend/routes/api/nginx/access_lists.js b/backend/routes/api/nginx/access_lists.js deleted file mode 100644 index d55c3ae1..00000000 --- a/backend/routes/api/nginx/access_lists.js +++ /dev/null @@ -1,148 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalAccessList = require('../../../internal/access-list'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/access-lists - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/access-lists - * - * Retrieve all access-lists - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalAccessList.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/access-lists - * - * Create a new access-list - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body) - .then((payload) => { - return internalAccessList.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific access-list - * - * /api/nginx/access-lists/123 - */ -router - .route('/:list_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/access-lists/123 - * - * Retrieve a specific access-list - */ - .get((req, res, next) => { - validator({ - required: ['list_id'], - additionalProperties: false, - properties: { - list_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - list_id: req.params.list_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalAccessList.get(res.locals.access, { - id: parseInt(data.list_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/access-lists/123 - * - * Update and existing access-list - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.list_id, 10); - return internalAccessList.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/access-lists/123 - * - * Delete and existing access-list - */ - .delete((req, res, next) => { - internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/certificates.js b/backend/routes/api/nginx/certificates.js deleted file mode 100644 index 553a0bba..00000000 --- a/backend/routes/api/nginx/certificates.js +++ /dev/null @@ -1,245 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalCertificate = require('../../../internal/certificate'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/certificates - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates - * - * Retrieve all certificates - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalCertificate.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/certificates - * - * Create a new certificate - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body) - .then((payload) => { - req.setTimeout(900000); // 15 minutes timeout - return internalCertificate.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific certificate - * - * /api/nginx/certificates/123 - */ -router - .route('/:certificate_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates/123 - * - * Retrieve a specific certificate - */ - .get((req, res, next) => { - validator({ - required: ['certificate_id'], - additionalProperties: false, - properties: { - certificate_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - certificate_id: req.params.certificate_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalCertificate.get(res.locals.access, { - id: parseInt(data.certificate_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/certificates/123 - * - * Update and existing certificate - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.certificate_id, 10); - return internalCertificate.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/certificates/123 - * - * Update and existing certificate - */ - .delete((req, res, next) => { - internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Upload Certs - * - * /api/nginx/certificates/123/upload - */ -router - .route('/:certificate_id/upload') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/123/upload - * - * Upload certificates - */ - .post((req, res, next) => { - if (!req.files) { - res.status(400) - .send({error: 'No files were uploaded'}); - } else { - internalCertificate.upload(res.locals.access, { - id: parseInt(req.params.certificate_id, 10), - files: req.files - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - } - }); - -/** - * Renew LE Certs - * - * /api/nginx/certificates/123/renew - */ -router - .route('/:certificate_id/renew') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/123/renew - * - * Renew certificate - */ - .post((req, res, next) => { - req.setTimeout(900000); // 15 minutes timeout - internalCertificate.renew(res.locals.access, { - id: parseInt(req.params.certificate_id, 10) - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Validate Certs before saving - * - * /api/nginx/certificates/validate - */ -router - .route('/validate') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/certificates/validate - * - * Validate certificates - */ - .post((req, res, next) => { - if (!req.files) { - res.status(400) - .send({error: 'No files were uploaded'}); - } else { - internalCertificate.validate({ - files: req.files - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - } - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/dead_hosts.js b/backend/routes/api/nginx/dead_hosts.js deleted file mode 100644 index 08b58f2d..00000000 --- a/backend/routes/api/nginx/dead_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalDeadHost = require('../../../internal/dead-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/dead-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/dead-hosts - * - * Retrieve all dead-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalDeadHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/dead-hosts - * - * Create a new dead-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalDeadHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific dead-host - * - * /api/nginx/dead-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/dead-hosts/123 - * - * Retrieve a specific dead-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalDeadHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/dead-hosts/123 - * - * Update and existing dead-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalDeadHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/dead-hosts/123 - * - * Update and existing dead-host - */ - .delete((req, res, next) => { - internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable dead-host - * - * /api/nginx/dead-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/dead-hosts/123/enable - */ - .post((req, res, next) => { - internalDeadHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable dead-host - * - * /api/nginx/dead-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/dead-hosts/123/disable - */ - .post((req, res, next) => { - internalDeadHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/proxy_hosts.js b/backend/routes/api/nginx/proxy_hosts.js deleted file mode 100644 index 6f933c3d..00000000 --- a/backend/routes/api/nginx/proxy_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalProxyHost = require('../../../internal/proxy-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/proxy-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/proxy-hosts - * - * Retrieve all proxy-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalProxyHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/proxy-hosts - * - * Create a new proxy-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalProxyHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific proxy-host - * - * /api/nginx/proxy-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/proxy-hosts/123 - * - * Retrieve a specific proxy-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalProxyHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/proxy-hosts/123 - * - * Update and existing proxy-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalProxyHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/proxy-hosts/123 - * - * Update and existing proxy-host - */ - .delete((req, res, next) => { - internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable proxy-host - * - * /api/nginx/proxy-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/proxy-hosts/123/enable - */ - .post((req, res, next) => { - internalProxyHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable proxy-host - * - * /api/nginx/proxy-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/proxy-hosts/123/disable - */ - .post((req, res, next) => { - internalProxyHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/redirection_hosts.js b/backend/routes/api/nginx/redirection_hosts.js deleted file mode 100644 index 4d44c112..00000000 --- a/backend/routes/api/nginx/redirection_hosts.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalRedirectionHost = require('../../../internal/redirection-host'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/redirection-hosts - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/redirection-hosts - * - * Retrieve all redirection-hosts - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/redirection-hosts - * - * Create a new redirection-host - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body) - .then((payload) => { - return internalRedirectionHost.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific redirection-host - * - * /api/nginx/redirection-hosts/123 - */ -router - .route('/:host_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/redirection-hosts/123 - * - * Retrieve a specific redirection-host - */ - .get((req, res, next) => { - validator({ - required: ['host_id'], - additionalProperties: false, - properties: { - host_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - host_id: req.params.host_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalRedirectionHost.get(res.locals.access, { - id: parseInt(data.host_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/redirection-hosts/123 - * - * Update and existing redirection-host - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.host_id, 10); - return internalRedirectionHost.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/redirection-hosts/123 - * - * Update and existing redirection-host - */ - .delete((req, res, next) => { - internalRedirectionHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable redirection-host - * - * /api/nginx/redirection-hosts/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/redirection-hosts/123/enable - */ - .post((req, res, next) => { - internalRedirectionHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable redirection-host - * - * /api/nginx/redirection-hosts/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/redirection-hosts/123/disable - */ - .post((req, res, next) => { - internalRedirectionHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/nginx/streams.js b/backend/routes/api/nginx/streams.js deleted file mode 100644 index 5e3fc28f..00000000 --- a/backend/routes/api/nginx/streams.js +++ /dev/null @@ -1,196 +0,0 @@ -const express = require('express'); -const validator = require('../../../lib/validator'); -const jwtdecode = require('../../../lib/express/jwt-decode'); -const internalStream = require('../../../internal/stream'); -const apiValidator = require('../../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/nginx/streams - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes - - /** - * GET /api/nginx/streams - * - * Retrieve all streams - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalStream.getAll(res.locals.access, data.expand, data.query); - }) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }) - - /** - * POST /api/nginx/streams - * - * Create a new stream - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body) - .then((payload) => { - return internalStream.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific stream - * - * /api/nginx/streams/123 - */ -router - .route('/:stream_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes - - /** - * GET /api/nginx/streams/123 - * - * Retrieve a specific stream - */ - .get((req, res, next) => { - validator({ - required: ['stream_id'], - additionalProperties: false, - properties: { - stream_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - stream_id: req.params.stream_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalStream.get(res.locals.access, { - id: parseInt(data.stream_id, 10), - expand: data.expand - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/nginx/streams/123 - * - * Update and existing stream - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = parseInt(req.params.stream_id, 10); - return internalStream.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/nginx/streams/123 - * - * Update and existing stream - */ - .delete((req, res, next) => { - internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Enable stream - * - * /api/nginx/streams/123/enable - */ -router - .route('/:host_id/enable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/streams/123/enable - */ - .post((req, res, next) => { - internalStream.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Disable stream - * - * /api/nginx/streams/123/disable - */ -router - .route('/:host_id/disable') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/nginx/streams/123/disable - */ - .post((req, res, next) => { - internalStream.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/reports.js b/backend/routes/api/reports.js deleted file mode 100644 index 9e2c98c8..00000000 --- a/backend/routes/api/reports.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalReport = require('../../internal/report'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/hosts') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /reports/hosts - */ - .get(jwtdecode(), (req, res, next) => { - internalReport.getHostsReport(res.locals.access) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/schema.js b/backend/routes/api/schema.js deleted file mode 100644 index fc6bd5bd..00000000 --- a/backend/routes/api/schema.js +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const swaggerJSON = require('../../doc/api.swagger.json'); -const PACKAGE = require('../../package.json'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /schema - */ - .get((req, res/*, next*/) => { - let proto = req.protocol; - if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && req.headers['x-forwarded-proto']) { - proto = req.headers['x-forwarded-proto']; - } - - let origin = proto + '://' + req.hostname; - if (typeof req.headers.origin !== 'undefined' && req.headers.origin) { - origin = req.headers.origin; - } - - swaggerJSON.info.version = PACKAGE.version; - swaggerJSON.servers[0].url = origin + '/api'; - res.status(200).send(swaggerJSON); - }); - -module.exports = router; diff --git a/backend/routes/api/settings.js b/backend/routes/api/settings.js deleted file mode 100644 index d08b2bf5..00000000 --- a/backend/routes/api/settings.js +++ /dev/null @@ -1,96 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalSetting = require('../../internal/setting'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/settings - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/settings - * - * Retrieve all settings - */ - .get((req, res, next) => { - internalSetting.getAll(res.locals.access) - .then((rows) => { - res.status(200) - .send(rows); - }) - .catch(next); - }); - -/** - * Specific setting - * - * /api/settings/something - */ -router - .route('/:setting_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /settings/something - * - * Retrieve a specific setting - */ - .get((req, res, next) => { - validator({ - required: ['setting_id'], - additionalProperties: false, - properties: { - setting_id: { - $ref: 'definitions#/definitions/setting_id' - } - } - }, { - setting_id: req.params.setting_id - }) - .then((data) => { - return internalSetting.get(res.locals.access, { - id: data.setting_id - }); - }) - .then((row) => { - res.status(200) - .send(row); - }) - .catch(next); - }) - - /** - * PUT /api/settings/something - * - * Update and existing setting - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body) - .then((payload) => { - payload.id = req.params.setting_id; - return internalSetting.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/tokens.js b/backend/routes/api/tokens.js deleted file mode 100644 index a21f998a..00000000 --- a/backend/routes/api/tokens.js +++ /dev/null @@ -1,54 +0,0 @@ -const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const internalToken = require('../../internal/token'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - - /** - * GET /tokens - * - * Get a new Token, given they already have a token they want to refresh - * We also piggy back on to this method, allowing admins to get tokens - * for services like Job board and Worker. - */ - .get(jwtdecode(), (req, res, next) => { - internalToken.getFreshToken(res.locals.access, { - expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null), - scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null) - }) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }) - - /** - * POST /tokens - * - * Create a new Token - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body) - .then((payload) => { - return internalToken.getTokenFromEmail(payload); - }) - .then((data) => { - res.status(200) - .send(data); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/routes/api/users.js b/backend/routes/api/users.js deleted file mode 100644 index 1c6bd0ad..00000000 --- a/backend/routes/api/users.js +++ /dev/null @@ -1,239 +0,0 @@ -const express = require('express'); -const validator = require('../../lib/validator'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const userIdFromMe = require('../../lib/express/user-id-from-me'); -const internalUser = require('../../internal/user'); -const apiValidator = require('../../lib/validator/api'); - -let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true -}); - -/** - * /api/users - */ -router - .route('/') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/users - * - * Retrieve all users - */ - .get((req, res, next) => { - validator({ - additionalProperties: false, - properties: { - expand: { - $ref: 'definitions#/definitions/expand' - }, - query: { - $ref: 'definitions#/definitions/query' - } - } - }, { - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null), - query: (typeof req.query.query === 'string' ? req.query.query : null) - }) - .then((data) => { - return internalUser.getAll(res.locals.access, data.expand, data.query); - }) - .then((users) => { - res.status(200) - .send(users); - }) - .catch(next); - }) - - /** - * POST /api/users - * - * Create a new User - */ - .post((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body) - .then((payload) => { - return internalUser.create(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user - * - * /api/users/123 - */ -router - .route('/:user_id') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * GET /users/123 or /users/me - * - * Retrieve a specific user - */ - .get((req, res, next) => { - validator({ - required: ['user_id'], - additionalProperties: false, - properties: { - user_id: { - $ref: 'definitions#/definitions/id' - }, - expand: { - $ref: 'definitions#/definitions/expand' - } - } - }, { - user_id: req.params.user_id, - expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) - }) - .then((data) => { - return internalUser.get(res.locals.access, { - id: data.user_id, - expand: data.expand, - omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id) - }); - }) - .then((user) => { - res.status(200) - .send(user); - }) - .catch(next); - }) - - /** - * PUT /api/users/123 - * - * Update and existing user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.update(res.locals.access, payload); - }) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }) - - /** - * DELETE /api/users/123 - * - * Update and existing user - */ - .delete((req, res, next) => { - internalUser.delete(res.locals.access, {id: req.params.user_id}) - .then((result) => { - res.status(200) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user auth - * - * /api/users/123/auth - */ -router - .route('/:user_id/auth') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * PUT /api/users/123/auth - * - * Update password for a user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.setPassword(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user permissions - * - * /api/users/123/permissions - */ -router - .route('/:user_id/permissions') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - .all(userIdFromMe) - - /** - * PUT /api/users/123/permissions - * - * Set some or all permissions for a user - */ - .put((req, res, next) => { - apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body) - .then((payload) => { - payload.id = req.params.user_id; - return internalUser.setPermissions(res.locals.access, payload); - }) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -/** - * Specific user login as - * - * /api/users/123/login - */ -router - .route('/:user_id/login') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/users/123/login - * - * Log in as a user - */ - .post((req, res, next) => { - internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)}) - .then((result) => { - res.status(201) - .send(result); - }) - .catch(next); - }); - -module.exports = router; diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json deleted file mode 100644 index 9895b87e..00000000 --- a/backend/schema/definitions.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "definitions", - "definitions": { - "id": { - "description": "Unique identifier", - "example": 123456, - "readOnly": true, - "type": "integer", - "minimum": 1 - }, - "setting_id": { - "description": "Unique identifier for a Setting", - "example": "default-site", - "readOnly": true, - "type": "string", - "minLength": 2 - }, - "token": { - "type": "string", - "minLength": 10 - }, - "expand": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "sort": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "field", - "dir" - ], - "additionalProperties": false, - "properties": { - "field": { - "type": "string" - }, - "dir": { - "type": "string", - "pattern": "^(asc|desc)$" - } - } - } - }, - "query": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "string", - "minLength": 1, - "maxLength": 255 - } - ] - }, - "criteria": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "object" - } - ] - }, - "fields": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "omit": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] - }, - "created_on": { - "description": "Date and time of creation", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "modified_on": { - "description": "Date and time of last update", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "user_id": { - "description": "User ID", - "example": 1234, - "type": "integer", - "minimum": 1 - }, - "certificate_id": { - "description": "Certificate ID", - "example": 1234, - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "string", - "pattern": "^new$" - } - ] - }, - "access_list_id": { - "description": "Access List ID", - "example": 1234, - "type": "integer", - "minimum": 0 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "email": { - "description": "Email Address", - "example": "john@example.com", - "format": "email", - "type": "string", - "minLength": 8, - "maxLength": 100 - }, - "password": { - "description": "Password", - "type": "string", - "minLength": 8, - "maxLength": 255 - }, - "domain_name": { - "description": "Domain Name", - "example": "jc21.com", - "type": "string", - "pattern": "^(?:[^.*]+\\.?)+[^.]$" - }, - "domain_names": { - "description": "Domain Names separated by a comma", - "example": "*.jc21.com,blog.jc21.com", - "type": "array", - "maxItems": 15, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^(?:\\*\\.)?(?:[^.*]+\\.?)+[^.]$" - } - }, - "http_code": { - "description": "Redirect HTTP Status Code", - "example": 302, - "type": "integer", - "minimum": 300, - "maximum": 308 - }, - "scheme": { - "description": "RFC Protocol", - "example": "HTTPS or $scheme", - "type": "string", - "minLength": 4 - }, - "enabled": { - "description": "Is Enabled", - "example": true, - "type": "boolean" - }, - "ssl_enabled": { - "description": "Is SSL Enabled", - "example": true, - "type": "boolean" - }, - "ssl_forced": { - "description": "Is SSL Forced", - "example": false, - "type": "boolean" - }, - "hsts_enabled": { - "description": "Is HSTS Enabled", - "example": false, - "type": "boolean" - }, - "hsts_subdomains": { - "description": "Is HSTS applicable to all subdomains", - "example": false, - "type": "boolean" - }, - "ssl_provider": { - "type": "string", - "pattern": "^(letsencrypt|other)$" - }, - "http2_support": { - "description": "HTTP2 Protocol Support", - "example": false, - "type": "boolean" - }, - "block_exploits": { - "description": "Should we block common exploits", - "example": true, - "type": "boolean" - }, - "caching_enabled": { - "description": "Should we cache assets", - "example": true, - "type": "boolean" - } - } -} diff --git a/backend/schema/endpoints/access-lists.json b/backend/schema/endpoints/access-lists.json deleted file mode 100644 index 404e3237..00000000 --- a/backend/schema/endpoints/access-lists.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/access-lists", - "title": "Access Lists", - "description": "Endpoints relating to Access Lists", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "name": { - "type": "string", - "description": "Name of the Access List" - }, - "directive": { - "type": "string", - "enum": ["allow", "deny"] - }, - "address": { - "oneOf": [ - { - "type": "string", - "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" - }, - { - "type": "string", - "pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$" - }, - { - "type": "string", - "pattern": "^all$" - } - ] - }, - "satisfy_any": { - "type": "boolean" - }, - "pass_auth": { - "type": "boolean" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "name": { - "$ref": "#/definitions/name" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Access Lists", - "href": "/nginx/access-lists", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Access List", - "href": "/nginx/access-list", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "satisfy_any": { - "$ref": "#/definitions/satisfy_any" - }, - "pass_auth": { - "$ref": "#/definitions/pass_auth" - }, - "items": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "username": { - "type": "string", - "minLength": 1 - }, - "password": { - "type": "string", - "minLength": 1 - } - } - } - }, - "clients": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "address": { - "$ref": "#/definitions/address" - }, - "directive": { - "$ref": "#/definitions/directive" - } - } - } - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Access List", - "href": "/nginx/access-list/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "satisfy_any": { - "$ref": "#/definitions/satisfy_any" - }, - "pass_auth": { - "$ref": "#/definitions/pass_auth" - }, - "items": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "username": { - "type": "string", - "minLength": 1 - }, - "password": { - "type": "string", - "minLength": 0 - } - } - } - }, - "clients": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "address": { - "$ref": "#/definitions/address" - }, - "directive": { - "$ref": "#/definitions/directive" - } - } - } - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Access List", - "href": "/nginx/access-list/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/certificates.json b/backend/schema/endpoints/certificates.json deleted file mode 100644 index 49fd6a7d..00000000 --- a/backend/schema/endpoints/certificates.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/certificates", - "title": "Certificates", - "description": "Endpoints relating to Certificates", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "provider": { - "$ref": "../definitions.json#/definitions/ssl_provider" - }, - "nice_name": { - "type": "string", - "description": "Nice Name for the custom certificate" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "expires_on": { - "description": "Date and time of expiration", - "format": "date-time", - "readOnly": true, - "type": "string" - }, - "meta": { - "type": "object", - "additionalProperties": false, - "properties": { - "letsencrypt_email": { - "type": "string", - "format": "email" - }, - "letsencrypt_agree": { - "type": "boolean" - }, - "dns_challenge": { - "type": "boolean" - }, - "dns_provider": { - "type": "string" - }, - "dns_provider_credentials": { - "type": "string" - }, - "propagation_seconds": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - } - ] - - } - } - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "provider": { - "$ref": "#/definitions/provider" - }, - "nice_name": { - "$ref": "#/definitions/nice_name" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "expires_on": { - "$ref": "#/definitions/expires_on" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Certificates", - "href": "/nginx/certificates", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Certificate", - "href": "/nginx/certificates", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "provider" - ], - "properties": { - "provider": { - "$ref": "#/definitions/provider" - }, - "nice_name": { - "$ref": "#/definitions/nice_name" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Certificate", - "href": "/nginx/certificates/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/dead-hosts.json b/backend/schema/endpoints/dead-hosts.json deleted file mode 100644 index 0c73c3be..00000000 --- a/backend/schema/endpoints/dead-hosts.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/dead-hosts", - "title": "404 Hosts", - "description": "Endpoints relating to 404 Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of 404 Hosts", - "href": "/nginx/dead-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new 404 Host", - "href": "/nginx/dead-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing 404 Host", - "href": "/nginx/dead-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json deleted file mode 100644 index 9a3fff2f..00000000 --- a/backend/schema/endpoints/proxy-hosts.json +++ /dev/null @@ -1,387 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/proxy-hosts", - "title": "Proxy Hosts", - "description": "Endpoints relating to Proxy Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "forward_scheme": { - "type": "string", - "enum": ["http", "https"] - }, - "forward_host": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "forward_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "../definitions.json#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "../definitions.json#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "description": "Allow Websocket Upgrade for all paths", - "example": true, - "type": "boolean" - }, - "access_list_id": { - "$ref": "../definitions.json#/definitions/access_list_id" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - }, - "locations": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "required": [ - "forward_scheme", - "forward_host", - "forward_port", - "path" - ], - "additionalProperties": false, - "properties": { - "id": { - "type": ["integer", "null"] - }, - "path": { - "type": "string", - "minLength": 1 - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "forward_path": { - "type": "string" - }, - "advanced_config": { - "type": "string" - } - } - } - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Proxy Hosts", - "href": "/nginx/proxy-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Proxy Host", - "href": "/nginx/proxy-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names", - "forward_scheme", - "forward_host", - "forward_port" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_host": { - "$ref": "#/definitions/forward_host" - }, - "forward_port": { - "$ref": "#/definitions/forward_port" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "caching_enabled": { - "$ref": "#/definitions/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "#/definitions/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "#/definitions/access_list_id" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "locations": { - "$ref": "#/definitions/locations" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Proxy Host", - "href": "/nginx/proxy-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/redirection-hosts.json b/backend/schema/endpoints/redirection-hosts.json deleted file mode 100644 index 14a46998..00000000 --- a/backend/schema/endpoints/redirection-hosts.json +++ /dev/null @@ -1,305 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/redirection-hosts", - "title": "Redirection Hosts", - "description": "Endpoints relating to Redirection Hosts", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "domain_names": { - "$ref": "../definitions.json#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "../definitions.json#/definitions/http_code" - }, - "forward_scheme": { - "$ref": "../definitions.json#/definitions/scheme" - }, - "forward_domain_name": { - "$ref": "../definitions.json#/definitions/domain_name" - }, - "preserve_path": { - "description": "Should the path be preserved", - "example": true, - "type": "boolean" - }, - "certificate_id": { - "$ref": "../definitions.json#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "../definitions.json#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../definitions.json#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../definitions.json#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "../definitions.json#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "../definitions.json#/definitions/block_exploits" - }, - "advanced_config": { - "type": "string" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_subdomains" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Redirection Hosts", - "href": "/nginx/redirection-hosts", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Redirection Host", - "href": "/nginx/redirection-hosts", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names", - "forward_scheme", - "forward_http_code", - "forward_domain_name" - ], - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "domain_names": { - "$ref": "#/definitions/domain_names" - }, - "forward_http_code": { - "$ref": "#/definitions/forward_http_code" - }, - "forward_scheme": { - "$ref": "#/definitions/forward_scheme" - }, - "forward_domain_name": { - "$ref": "#/definitions/forward_domain_name" - }, - "preserve_path": { - "$ref": "#/definitions/preserve_path" - }, - "certificate_id": { - "$ref": "#/definitions/certificate_id" - }, - "ssl_forced": { - "$ref": "#/definitions/ssl_forced" - }, - "hsts_enabled": { - "$ref": "#/definitions/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "#/definitions/hsts_enabled" - }, - "http2_support": { - "$ref": "#/definitions/http2_support" - }, - "block_exploits": { - "$ref": "#/definitions/block_exploits" - }, - "advanced_config": { - "$ref": "#/definitions/advanced_config" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Redirection Host", - "href": "/nginx/redirection-hosts/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/settings.json b/backend/schema/endpoints/settings.json deleted file mode 100644 index 29e2865a..00000000 --- a/backend/schema/endpoints/settings.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/settings", - "title": "Settings", - "description": "Endpoints relating to Settings", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/setting_id" - }, - "name": { - "description": "Name", - "example": "Default Site", - "type": "string", - "minLength": 2, - "maxLength": 100 - }, - "description": { - "description": "Description", - "example": "Default Site", - "type": "string", - "minLength": 2, - "maxLength": 255 - }, - "value": { - "description": "Value", - "example": "404", - "type": "string", - "maxLength": 255 - }, - "meta": { - "type": "object" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Settings", - "href": "/settings", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Setting", - "href": "/settings/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/value" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - } - ], - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "name": { - "$ref": "#/definitions/description" - }, - "description": { - "$ref": "#/definitions/description" - }, - "value": { - "$ref": "#/definitions/value" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } -} diff --git a/backend/schema/endpoints/streams.json b/backend/schema/endpoints/streams.json deleted file mode 100644 index e93e1ff3..00000000 --- a/backend/schema/endpoints/streams.json +++ /dev/null @@ -1,223 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/streams", - "title": "Streams", - "description": "Endpoints relating to Streams", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "incoming_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "forward_ip": { - "type": "string", - "format": "ipv4" - }, - "forwarding_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "tcp_forwarding": { - "type": "boolean" - }, - "udp_forwarding": { - "type": "boolean" - }, - "enabled": { - "$ref": "../definitions.json#/definitions/enabled" - }, - "meta": { - "type": "object" - } - }, - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forward_ip": { - "$ref": "#/definitions/forward_ip" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "enabled": { - "$ref": "#/definitions/enabled" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Steams", - "href": "/nginx/streams", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new Stream", - "href": "/nginx/streams", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "incoming_port", - "forward_ip", - "forwarding_port" - ], - "properties": { - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forward_ip": { - "$ref": "#/definitions/forward_ip" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forward_ip": { - "$ref": "#/definitions/forward_ip" - }, - "forwarding_port": { - "$ref": "#/definitions/forwarding_port" - }, - "tcp_forwarding": { - "$ref": "#/definitions/tcp_forwarding" - }, - "udp_forwarding": { - "$ref": "#/definitions/udp_forwarding" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Enable", - "description": "Enables a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}/enable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Disable", - "description": "Disables a existing Stream", - "href": "/nginx/streams/{definitions.identity.example}/disable", - "access": "private", - "method": "POST", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - } - ] -} diff --git a/backend/schema/endpoints/tokens.json b/backend/schema/endpoints/tokens.json deleted file mode 100644 index 920af63f..00000000 --- a/backend/schema/endpoints/tokens.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/tokens", - "title": "Token", - "description": "Tokens are required to authenticate against the API", - "stability": "stable", - "type": "object", - "definitions": { - "identity": { - "description": "Email Address or other 3rd party providers identifier", - "example": "john@example.com", - "type": "string" - }, - "secret": { - "description": "A password or key", - "example": "correct horse battery staple", - "type": "string" - }, - "token": { - "description": "JWT", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk", - "type": "string" - }, - "expires": { - "description": "Token expiry time", - "format": "date-time", - "type": "string" - }, - "scope": { - "description": "Scope of the Token, defaults to 'user'", - "example": "user", - "type": "string" - } - }, - "links": [ - { - "title": "Create", - "description": "Creates a new token.", - "href": "/tokens", - "access": "public", - "method": "POST", - "rel": "create", - "schema": { - "type": "object", - "required": [ - "identity", - "secret" - ], - "properties": { - "identity": { - "$ref": "#/definitions/identity" - }, - "secret": { - "$ref": "#/definitions/secret" - }, - "scope": { - "$ref": "#/definitions/scope" - } - } - }, - "targetSchema": { - "type": "object", - "properties": { - "token": { - "$ref": "#/definitions/token" - }, - "expires": { - "$ref": "#/definitions/expires" - } - } - } - }, - { - "title": "Refresh", - "description": "Returns a new token.", - "href": "/tokens", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": {}, - "targetSchema": { - "type": "object", - "properties": { - "token": { - "$ref": "#/definitions/token" - }, - "expires": { - "$ref": "#/definitions/expires" - }, - "scope": { - "$ref": "#/definitions/scope" - } - } - } - } - ] -} diff --git a/backend/schema/endpoints/users.json b/backend/schema/endpoints/users.json deleted file mode 100644 index 42f44eac..00000000 --- a/backend/schema/endpoints/users.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "endpoints/users", - "title": "Users", - "description": "Endpoints relating to Users", - "stability": "stable", - "type": "object", - "definitions": { - "id": { - "$ref": "../definitions.json#/definitions/id" - }, - "created_on": { - "$ref": "../definitions.json#/definitions/created_on" - }, - "modified_on": { - "$ref": "../definitions.json#/definitions/modified_on" - }, - "name": { - "description": "Name", - "example": "Jamie Curnow", - "type": "string", - "minLength": 2, - "maxLength": 100 - }, - "nickname": { - "description": "Nickname", - "example": "Jamie", - "type": "string", - "minLength": 2, - "maxLength": 50 - }, - "email": { - "$ref": "../definitions.json#/definitions/email" - }, - "avatar": { - "description": "Avatar", - "example": "http://somewhere.jpg", - "type": "string", - "minLength": 2, - "maxLength": 150, - "readOnly": true - }, - "roles": { - "description": "Roles", - "example": [ - "admin" - ], - "type": "array" - }, - "is_disabled": { - "description": "Is Disabled", - "example": false, - "type": "boolean" - } - }, - "links": [ - { - "title": "List", - "description": "Returns a list of Users", - "href": "/users", - "access": "private", - "method": "GET", - "rel": "self", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "array", - "items": { - "$ref": "#/properties" - } - } - }, - { - "title": "Create", - "description": "Creates a new User", - "href": "/users", - "access": "private", - "method": "POST", - "rel": "create", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "required": [ - "name", - "nickname", - "email" - ], - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - }, - "auth": { - "type": "object", - "description": "Auth Credentials", - "example": { - "type": "password", - "secret": "bigredhorsebanana" - } - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Update", - "description": "Updates a existing User", - "href": "/users/{definitions.identity.example}", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - } - } - }, - "targetSchema": { - "properties": { - "$ref": "#/properties" - } - } - }, - { - "title": "Delete", - "description": "Deletes a existing User", - "href": "/users/{definitions.identity.example}", - "access": "private", - "method": "DELETE", - "rel": "delete", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Set Password", - "description": "Sets a password for an existing User", - "href": "/users/{definitions.identity.example}/auth", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "required": [ - "type", - "secret" - ], - "properties": { - "type": { - "type": "string", - "pattern": "^password$" - }, - "current": { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "secret": { - "type": "string", - "minLength": 8, - "maxLength": 64 - } - } - }, - "targetSchema": { - "type": "boolean" - } - }, - { - "title": "Set Permissions", - "description": "Sets Permissions for a User", - "href": "/users/{definitions.identity.example}/permissions", - "access": "private", - "method": "PUT", - "rel": "update", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - }, - "schema": { - "type": "object", - "properties": { - "visibility": { - "type": "string", - "pattern": "^(all|user)$" - }, - "access_lists": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "dead_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "proxy_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "redirection_hosts": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "streams": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - }, - "certificates": { - "type": "string", - "pattern": "^(hidden|view|manage)$" - } - } - }, - "targetSchema": { - "type": "boolean" - } - } - ], - "properties": { - "id": { - "$ref": "#/definitions/id" - }, - "created_on": { - "$ref": "#/definitions/created_on" - }, - "modified_on": { - "$ref": "#/definitions/modified_on" - }, - "name": { - "$ref": "#/definitions/name" - }, - "nickname": { - "$ref": "#/definitions/nickname" - }, - "email": { - "$ref": "#/definitions/email" - }, - "avatar": { - "$ref": "#/definitions/avatar" - }, - "roles": { - "$ref": "#/definitions/roles" - }, - "is_disabled": { - "$ref": "#/definitions/is_disabled" - } - } -} diff --git a/backend/schema/examples.json b/backend/schema/examples.json deleted file mode 100644 index 37bc6c4d..00000000 --- a/backend/schema/examples.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "examples", - "type": "object", - "definitions": { - "name": { - "description": "Name", - "example": "John Smith", - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "auth_header": { - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk", - "X-API-Version": "next" - }, - "token": { - "type": "string", - "description": "JWT", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk" - } - } -} diff --git a/backend/schema/index.json b/backend/schema/index.json deleted file mode 100644 index 6e7d1c8a..00000000 --- a/backend/schema/index.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "root", - "title": "Nginx Proxy Manager REST API", - "description": "This is the Nginx Proxy Manager REST API", - "version": "2.0.0", - "links": [ - { - "href": "http://npm.example.com/api", - "rel": "self" - } - ], - "properties": { - "tokens": { - "$ref": "endpoints/tokens.json" - }, - "users": { - "$ref": "endpoints/users.json" - }, - "proxy-hosts": { - "$ref": "endpoints/proxy-hosts.json" - }, - "redirection-hosts": { - "$ref": "endpoints/redirection-hosts.json" - }, - "dead-hosts": { - "$ref": "endpoints/dead-hosts.json" - }, - "streams": { - "$ref": "endpoints/streams.json" - }, - "certificates": { - "$ref": "endpoints/certificates.json" - }, - "access-lists": { - "$ref": "endpoints/access-lists.json" - }, - "settings": { - "$ref": "endpoints/settings.json" - } - } -} diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh new file mode 100755 index 00000000..bf6bf4c8 --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +BLUE='\E[1;34m' +YELLOW='\E[1;33m' +RESET='\E[0m' +RESULT=0 + +# go files: incomplete comment check +INCOMPLETE_COMMENTS=$(find . -iname "*.go*" | grep -v " " | xargs grep --colour -H -n -E "^\s*\/\/\s*[A-Z]\w+ \.{3}" 2>/dev/null) +if [[ -n "$INCOMPLETE_COMMENTS" ]]; then + echo -e "${BLUE}❯ ${YELLOW}WARN: Please fix incomplete exported comments:${RESET}" + echo -e "${RED}${INCOMPLETE_COMMENTS}${RESET}" + echo + # RESULT=1 +fi + +if ! golangci-lint run -E goimports -E maligned ./...; then + exit 1 +fi + +exit "$RESULT" diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh new file mode 100755 index 00000000..92a0a02f --- /dev/null +++ b/backend/scripts/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +export RICHGO_FORCE_COLOR=1 + +richgo test -bench=. -cover -v ./internal/... diff --git a/backend/setup.js b/backend/setup.js deleted file mode 100644 index b25ffc00..00000000 --- a/backend/setup.js +++ /dev/null @@ -1,209 +0,0 @@ -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); -const logger = require('./logger').setup; -const certificateModel = require('./models/certificate'); -const userModel = require('./models/user'); -const userPermissionModel = require('./models/user_permission'); -const utils = require('./lib/utils'); -const authModel = require('./models/auth'); -const settingModel = require('./models/setting'); -const dns_plugins = require('./global/certbot-dns-plugins'); -const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG; - -/** - * Creates a new JWT RSA Keypair if not alread set on the config - * - * @returns {Promise} - */ -const setupJwt = () => { - return new Promise((resolve, reject) => { - // Now go and check if the jwt gpg keys have been created and if not, create them - if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) { - logger.info('Creating a new JWT key pair...'); - - // jwt keys are not configured properly - const filename = config.util.getEnv('NODE_CONFIG_DIR') + '/' + (config.util.getEnv('NODE_ENV') || 'default') + '.json'; - let config_data = {}; - - try { - config_data = require(filename); - } catch (err) { - // do nothing - if (debug_mode) { - logger.debug(filename + ' config file could not be required'); - } - } - - // Now create the keys and save them in the config. - let key = new NodeRSA({ b: 2048 }); - key.generateKeyPair(); - - config_data.jwt = { - key: key.exportKey('private').toString(), - pub: key.exportKey('public').toString(), - }; - - // Write config - fs.writeFile(filename, JSON.stringify(config_data, null, 2), (err) => { - if (err) { - logger.error('Could not write JWT key pair to config file: ' + filename); - reject(err); - } else { - logger.info('Wrote JWT key pair to config file: ' + filename); - delete require.cache[require.resolve('config')]; - resolve(); - } - }); - } else { - // JWT key pair exists - if (debug_mode) { - logger.debug('JWT Keypair already exists'); - } - - resolve(); - } - }); -}; - -/** - * Creates a default admin users if one doesn't already exist in the database - * - * @returns {Promise} - */ -const setupDefaultUser = () => { - return userModel - .query() - .select(userModel.raw('COUNT(`id`) as `count`')) - .where('is_deleted', 0) - .first() - .then((row) => { - if (!row.count) { - // Create a new user and set password - logger.info('Creating a new user: admin@example.com with password: changeme'); - - let data = { - is_deleted: 0, - email: 'admin@example.com', - name: 'Administrator', - nickname: 'Admin', - avatar: '', - roles: ['admin'], - }; - - return userModel - .query() - .insertAndFetch(data) - .then((user) => { - return authModel - .query() - .insert({ - user_id: user.id, - type: 'password', - secret: 'changeme', - meta: {}, - }) - .then(() => { - return userPermissionModel.query().insert({ - user_id: user.id, - visibility: 'all', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - dead_hosts: 'manage', - streams: 'manage', - access_lists: 'manage', - certificates: 'manage', - }); - }); - }) - .then(() => { - logger.info('Initial admin setup completed'); - }); - } else if (debug_mode) { - logger.debug('Admin user setup not required'); - } - }); -}; - -/** - * Creates default settings if they don't already exist in the database - * - * @returns {Promise} - */ -const setupDefaultSettings = () => { - return settingModel - .query() - .select(settingModel.raw('COUNT(`id`) as `count`')) - .where({id: 'default-site'}) - .first() - .then((row) => { - if (!row.count) { - settingModel - .query() - .insert({ - id: 'default-site', - name: 'Default Site', - description: 'What to show when Nginx is hit with an unknown Host', - value: 'congratulations', - meta: {}, - }) - .then(() => { - logger.info('Default settings added'); - }); - } - if (debug_mode) { - logger.debug('Default setting setup not required'); - } - }); -}; - -/** - * Installs all Certbot plugins which are required for an installed certificate - * - * @returns {Promise} - */ -const setupCertbotPlugins = () => { - return certificateModel - .query() - .where('is_deleted', 0) - .andWhere('provider', 'letsencrypt') - .then((certificates) => { - if (certificates && certificates.length) { - let plugins = []; - let promises = []; - - certificates.map(function (certificate) { - if (certificate.meta && certificate.meta.dns_challenge === true) { - const dns_plugin = dns_plugins[certificate.meta.dns_provider]; - const packages_to_install = `${dns_plugin.package_name}==${dns_plugin.package_version} ${dns_plugin.dependencies}`; - - if (plugins.indexOf(packages_to_install) === -1) plugins.push(packages_to_install); - - // Make sure credentials file exists - const credentials_loc = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'; }'; - promises.push(utils.exec(credentials_cmd)); - } - }); - - if (plugins.length) { - const install_cmd = 'pip install ' + plugins.join(' '); - promises.push(utils.exec(install_cmd)); - } - - if (promises.length) { - return Promise.all(promises) - .then(() => { - logger.info('Added Certbot plugins ' + plugins.join(', ')); - }); - } - } - }); -}; - -module.exports = function () { - return setupJwt() - .then(setupDefaultUser) - .then(setupDefaultSettings) - .then(setupCertbotPlugins); -}; diff --git a/backend/templates/_assets.conf b/backend/templates/_assets.conf deleted file mode 100644 index dcb183c5..00000000 --- a/backend/templates/_assets.conf +++ /dev/null @@ -1,4 +0,0 @@ -{% if caching_enabled == 1 or caching_enabled == true -%} - # Asset Caching - include conf.d/include/assets.conf; -{% endif %} \ No newline at end of file diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf deleted file mode 100644 index 06ca7bb8..00000000 --- a/backend/templates/_certificates.conf +++ /dev/null @@ -1,14 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if certificate.provider == "letsencrypt" %} - # Let's Encrypt SSL - include conf.d/include/letsencrypt-acme-challenge.conf; - include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; -{% else %} - # Custom SSL - ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem; - ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem; -{% endif %} -{% endif %} - diff --git a/backend/templates/_exploits.conf b/backend/templates/_exploits.conf deleted file mode 100644 index 002970d5..00000000 --- a/backend/templates/_exploits.conf +++ /dev/null @@ -1,4 +0,0 @@ -{% if block_exploits == 1 or block_exploits == true %} - # Block Exploits - include conf.d/include/block-exploits.conf; -{% endif %} \ No newline at end of file diff --git a/backend/templates/_forced_ssl.conf b/backend/templates/_forced_ssl.conf deleted file mode 100644 index 7fade20c..00000000 --- a/backend/templates/_forced_ssl.conf +++ /dev/null @@ -1,6 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} - # Force SSL - include conf.d/include/force-ssl.conf; -{% endif %} -{% endif %} \ No newline at end of file diff --git a/backend/templates/_header_comment.conf b/backend/templates/_header_comment.conf deleted file mode 100644 index 8f996d34..00000000 --- a/backend/templates/_header_comment.conf +++ /dev/null @@ -1,3 +0,0 @@ -# ------------------------------------------------------------ -# {{ domain_names | join: ", " }} -# ------------------------------------------------------------ \ No newline at end of file diff --git a/backend/templates/_hsts.conf b/backend/templates/_hsts.conf deleted file mode 100644 index 11aecf24..00000000 --- a/backend/templates/_hsts.conf +++ /dev/null @@ -1,8 +0,0 @@ -{% if certificate and certificate_id > 0 -%} -{% if ssl_forced == 1 or ssl_forced == true %} -{% if hsts_enabled == 1 or hsts_enabled == true %} - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always; -{% endif %} -{% endif %} -{% endif %} diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf deleted file mode 100644 index 8f40bea2..00000000 --- a/backend/templates/_listen.conf +++ /dev/null @@ -1,15 +0,0 @@ - listen 80; -{% if ipv6 -%} - listen [::]:80; -{% else -%} - #listen [::]:80; -{% endif %} -{% if certificate -%} - listen 443 ssl{% if http2_support %} http2{% endif %}; -{% if ipv6 -%} - listen [::]:443; -{% else -%} - #listen [::]:443; -{% endif %} -{% endif %} - server_name {{ domain_names | join: " " }}; diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf deleted file mode 100644 index 5a7a6abe..00000000 --- a/backend/templates/_location.conf +++ /dev/null @@ -1,45 +0,0 @@ - location {{ path }} { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; - - {% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{ access_list_id }}; - - {{ access_list.passauth }} - {% endif %} - - # Access Rules - {% for client in access_list.clients %} - {{- client.rule -}}; - {% endfor %}deny all; - - # Access checks must... - {% if access_list.satisfy %} - {{ access_list.satisfy }}; - {% endif %} - - {% endif %} - - {% include "_assets.conf" %} - {% include "_exploits.conf" %} - - {% include "_forced_ssl.conf" %} - {% include "_hsts.conf" %} - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - {% endif %} - - - {{ advanced_config }} - } - diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf deleted file mode 100644 index be53f6df..00000000 --- a/backend/templates/dead_host.conf +++ /dev/null @@ -1,22 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - - access_log /data/logs/dead_host-{{ id }}.log standard; - -{{ advanced_config }} - -{% if use_default_location %} - location / { -{% include "_hsts.conf" %} - return 404; - } -{% endif %} - -} -{% endif %} diff --git a/backend/templates/default.conf b/backend/templates/default.conf deleted file mode 100644 index 56b67090..00000000 --- a/backend/templates/default.conf +++ /dev/null @@ -1,37 +0,0 @@ -# ------------------------------------------------------------ -# Default Site -# ------------------------------------------------------------ -{% if value == "congratulations" %} -# Skipping output, congratulations page configration is baked in. -{%- else %} -server { - listen 80 default; -{% if ipv6 -%} - listen [::]:80; -{% else -%} - #listen [::]:80; -{% endif %} - server_name default-host.localhost; - access_log /data/logs/default_host.log combined; -{% include "_exploits.conf" %} - -{%- if value == "404" %} - location / { - return 404; - } -{% endif %} - -{%- if value == "redirect" %} - location / { - return 301 {{ meta.redirect }}; - } -{%- endif %} - -{%- if value == "html" %} - root /data/nginx/default_www; - location / { - try_files $uri /index.html; - } -{%- endif %} -} -{% endif %} diff --git a/backend/templates/ip_ranges.conf b/backend/templates/ip_ranges.conf deleted file mode 100644 index 8ede2bd9..00000000 --- a/backend/templates/ip_ranges.conf +++ /dev/null @@ -1,3 +0,0 @@ -{% for range in ip_ranges %} -set_real_ip_from {{ range }}; -{% endfor %} \ No newline at end of file diff --git a/backend/templates/letsencrypt-request.conf b/backend/templates/letsencrypt-request.conf deleted file mode 100644 index cda2f892..00000000 --- a/backend/templates/letsencrypt-request.conf +++ /dev/null @@ -1,18 +0,0 @@ -{% include "_header_comment.conf" %} - -server { - listen 80; -{% if ipv6 -%} - listen [::]:80; -{% endif %} - - server_name {{ domain_names | join: " " }}; - - access_log /data/logs/letsencrypt-requests.log standard; - - include conf.d/include/letsencrypt-acme-challenge.conf; - - location / { - return 404; - } -} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf deleted file mode 100644 index 538b85e5..00000000 --- a/backend/templates/proxy_host.conf +++ /dev/null @@ -1,70 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { - set $forward_scheme {{ forward_scheme }}; - set $server "{{ forward_host }}"; - set $port {{ forward_port }}; - -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_assets.conf" %} -{% include "_exploits.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - -{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection $http_connection; -proxy_http_version 1.1; -{% endif %} - - - access_log /data/logs/proxy_host-{{ id }}.log proxy; - -{{ advanced_config }} - -{{ locations }} - -{% if use_default_location %} - - location / { - - {% if access_list_id > 0 %} - {% if access_list.items.length > 0 %} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{ access_list_id }}; - - {{ access_list.passauth }} - {% endif %} - - # Access Rules - {% for client in access_list.clients %} - {{- client.rule -}}; - {% endfor %}deny all; - - # Access checks must... - {% if access_list.satisfy %} - {{ access_list.satisfy }}; - {% endif %} - - {% endif %} - -{% include "_hsts.conf" %} - - {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - {% endif %} - - # Proxy! - include conf.d/include/proxy.conf; - } -{% endif %} - - # Custom - include /data/nginx/custom/server_proxy[.]conf; -} -{% endif %} diff --git a/backend/templates/redirection_host.conf b/backend/templates/redirection_host.conf deleted file mode 100644 index f42e146b..00000000 --- a/backend/templates/redirection_host.conf +++ /dev/null @@ -1,31 +0,0 @@ -{% include "_header_comment.conf" %} - -{% if enabled %} -server { -{% include "_listen.conf" %} -{% include "_certificates.conf" %} -{% include "_assets.conf" %} -{% include "_exploits.conf" %} -{% include "_hsts.conf" %} -{% include "_forced_ssl.conf" %} - - access_log /data/logs/redirection_host-{{ id }}.log standard; - -{{ advanced_config }} - -{% if use_default_location %} - location / { -{% include "_hsts.conf" %} - - {% if preserve_path == 1 or preserve_path == true %} - return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri; - {% else %} - return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}; - {% endif %} - } -{% endif %} - - # Custom - include /data/nginx/custom/server_redirect[.]conf; -} -{% endif %} diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf deleted file mode 100644 index 05f68772..00000000 --- a/backend/templates/stream.conf +++ /dev/null @@ -1,37 +0,0 @@ -# ------------------------------------------------------------ -# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} -# ------------------------------------------------------------ - -{% if enabled %} -{% if tcp_forwarding == 1 or tcp_forwarding == true -%} -server { - listen {{ incoming_port }}; -{% if ipv6 -%} - listen [::]:{{ incoming_port }}; -{% else -%} - #listen [::]:{{ incoming_port }}; -{% endif %} - - proxy_pass {{ forward_ip }}:{{ forwarding_port }}; - - # Custom - include /data/nginx/custom/server_stream[.]conf; - include /data/nginx/custom/server_stream_tcp[.]conf; -} -{% endif %} -{% if udp_forwarding == 1 or udp_forwarding == true %} -server { - listen {{ incoming_port }} udp; -{% if ipv6 -%} - listen [::]:{{ incoming_port }} udp; -{% else -%} - #listen [::]:{{ incoming_port }} udp; -{% endif %} - proxy_pass {{ forward_ip }}:{{ forwarding_port }}; - - # Custom - include /data/nginx/custom/server_stream[.]conf; - include /data/nginx/custom/server_stream_udp[.]conf; -} -{% endif %} -{% endif %} \ No newline at end of file diff --git a/backend/yarn.lock b/backend/yarn.lock deleted file mode 100644 index 84180c26..00000000 --- a/backend/yarn.lock +++ /dev/null @@ -1,3757 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@apidevtools/json-schema-ref-parser@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#9eb749499b3f8d919e90bb141e4b6f67aee4692d" - integrity sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw== - dependencies: - "@jsdevtools/ono" "^7.1.0" - call-me-maybe "^1.0.1" - js-yaml "^3.13.1" - -"@babel/code-frame@^7.0.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@jsdevtools/ono@^7.1.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== - -acorn@^7.1.1: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== - -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-align@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== - dependencies: - string-width "^3.0.0" - -ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== - dependencies: - type-fest "^0.11.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg= - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batchflow@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/batchflow/-/batchflow-0.4.0.tgz#7d419df79b6b7587b06f9ea34f96ccef6f74e5b5" - integrity sha1-fUGd95trdYewb56jT5bM72905bU= - -bcrypt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.0.tgz#051407c7cd5ffbfb773d541ca3760ea0754e37e2" - integrity sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg== - dependencies: - node-addon-api "^3.0.0" - node-pre-gyp "0.15.0" - -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== - -blueimp-md5@^2.16.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.17.0.tgz#f4fcac088b115f7b4045f19f5da59e9d01b1bb96" - integrity sha512-x5PKJHY5rHQYaADj6NwPUR2QRCUVSggPzrUKkeENpj871o9l9IefJbO2jkT5UvYykeOK9dx0VmkIo6dZ+vThYw== - -body-parser@1.19.0, body-parser@^1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -boxen@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" - widest-line "^3.1.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - -busboy@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" - integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== - dependencies: - dicer "0.3.0" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8= - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chokidar@^3.2.2: - version "3.4.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" - integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.4.0" - optionalDependencies: - fsevents "~2.1.2" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cli-boxes@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" - integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7" - integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg== - -commander@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -config@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/config/-/config-3.3.1.tgz#b6a70e2908a43b98ed20be7e367edf0cc8ed5a19" - integrity sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q== - dependencies: - json5 "^2.1.1" - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -db-errors@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2" - integrity sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng== - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4.1.1, debug@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -dicer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" - -diskdb@^0.1.17: - version "0.1.17" - resolved "https://registry.yarnpkg.com/diskdb/-/diskdb-0.1.17.tgz#8abd095196b33b406791f1494b6b13b4422240c4" - integrity sha1-ir0JUZazO0BnkfFJS2sTtEIiQMQ= - dependencies: - chalk "^0.4.0" - merge "^1.1.3" - node-uuid "^1.4.1" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== - dependencies: - is-obj "^2.0.0" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -email-validator@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" - integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-plugin-align-assignments@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-align-assignments/-/eslint-plugin-align-assignments-1.1.2.tgz#83e1a8a826d4adf29e82b52d0bb39c88b301b576" - integrity sha512-I1ZJgk9EjHfGVU9M2Ex8UkVkkjLL5Y9BS6VNnQHq79eHj2H4/Cgxf36lQSUTLgm2ntB03A2NtF+zg9fyi5vChg== - -eslint-scope@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint@^6.8.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^7.0.0" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.3" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -esm@^3.2.25: - version "3.2.25" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== - dependencies: - acorn "^7.1.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.1.0, estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -express-fileupload@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.1.9.tgz#e798e9318394ed5083e56217ad6cda576da465d2" - integrity sha512-f2w0aoe7lj3NeD8a4MXmYQsqir3Z66I08l9AKq04QbFUAjeZNmPwTlR5Lx2NGwSu/PslsAjGC38MWzo5tTjoBg== - dependencies: - busboy "^0.3.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" - integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== - dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -flagged-respawn@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" - integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fs-minipass@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" - integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getopts@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" - integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== - -glob-parent@^5.0.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" - integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== - dependencies: - ini "^1.3.5" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -gravatar@^1.8.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.8.1.tgz#743bbdf3185c3433172e00e0e6ff5f6b30c58997" - integrity sha512-18frnfVp4kRYkM/eQW32Mfwlsh/KMbwd3S6nkescBZHioobflFEFHsvM71qZAkUSLNifyi2uoI+TuGxJAnQIOA== - dependencies: - blueimp-md5 "^2.16.0" - email-validator "^2.0.4" - querystring "0.2.0" - yargs "^15.4.1" - -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -html-entities@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -iconv-lite@0.2.11: - version "0.2.11" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" - integrity sha1-HOYKOleGSiktEyH/RgnKS7llrcg= - -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= - -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inquirer@^7.0.0: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -interpret@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-ref-parser@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz#7c758fac2cf822c05e837abd0a13f8fa2c15ffd4" - integrity sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ== - dependencies: - "@apidevtools/json-schema-ref-parser" "8.0.0" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json5@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -knex@^0.20.13: - version "0.20.15" - resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.15.tgz#b7e9e1efd9cf35d214440d9439ed21153574679d" - integrity sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q== - dependencies: - colorette "1.1.0" - commander "^4.1.1" - debug "4.1.1" - esm "^3.2.25" - getopts "2.2.5" - inherits "~2.0.4" - interpret "^2.0.0" - liftoff "3.1.0" - lodash "^4.17.15" - mkdirp "^0.5.1" - pg-connection-string "2.1.0" - tarn "^2.0.0" - tildify "2.0.0" - uuid "^7.0.1" - v8flags "^3.1.3" - -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -liftoff@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" - integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== - dependencies: - extend "^3.0.0" - findup-sync "^3.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" - -liquidjs@^9.11.10: - version "9.15.0" - resolved "https://registry.yarnpkg.com/liquidjs/-/liquidjs-9.15.0.tgz#03e8c13aeda89801a346c614b0802f320458d0ac" - integrity sha512-wRPNfMx6X3GGEDqTlBpw7VMo8ylKkzLYTcd7eeaDeYnZyR5BqUgF9tZy3FdPCHV2N/BassGKmlmlpJiRXGFOqg== - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-iterator@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" - integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== - dependencies: - kind-of "^6.0.2" - -map-cache@^0.2.0, map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge@^1.1.3: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.0.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -moment@^2.24.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -mysql@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== - dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" - -nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -needle@^2.2.1, needle@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.0.tgz#e6fc4b3cc6c25caed7554bd613a5cf0bac8c31c0" - integrity sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-addon-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" - integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== - -node-pre-gyp@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087" - integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA== - dependencies: - 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" - -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - 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" - -node-rsa@^1.0.8: - version "1.1.1" - resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" - integrity sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw== - dependencies: - asn1 "^0.2.4" - -node-uuid@^1.4.1: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= - -nodemon@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" - integrity sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ== - dependencies: - chokidar "^3.2.2" - debug "^3.2.6" - ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.7" - semver "^5.7.1" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.2" - update-notifier "^4.0.0" - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== - -npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.pick@^1.2.0, object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -objection@^2.1.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/objection/-/objection-2.2.2.tgz#1a3c9010270e3677940d2bc91aeaeb3c0f103800" - integrity sha512-+1Ap7u9NQRochzDW5/BggUlKi94JfZGTJwQJuNXo8DwmAb8czEirvxcWBcX91/MmQq0BQUJjM4RPSiZhnkkWQw== - dependencies: - ajv "^6.12.0" - db-errors "^0.2.3" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.1.tgz#5c8016847b0d67fcedb7eef254751cfcdc7e9418" - integrity sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-filepath@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= - dependencies: - path-root-regex "^0.1.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path@^0.12.7: - version "0.12.7" - resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" - integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= - dependencies: - process "^0.11.1" - util "^0.10.3" - -pg-connection-string@0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" - integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= - -pg-connection-string@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502" - integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg== - -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-packet-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz#e45c3ae678b901a2873af1e17b92d787962ef914" - integrity sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg== - -pg-pool@^2.0.10: - version "2.0.10" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.10.tgz#842ee23b04e86824ce9d786430f8365082d81c4a" - integrity sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg== - -pg-types@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - pg-int8 "1.0.1" - postgres-array "~2.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.4" - postgres-interval "^1.1.0" - -pg@^7.12.1: - version "7.18.2" - resolved "https://registry.yarnpkg.com/pg/-/pg-7.18.2.tgz#4e219f05a00aff4db6aab1ba02f28ffa4513b0bb" - integrity sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "0.1.3" - pg-packet-stream "^1.1.0" - pg-pool "^2.0.10" - pg-types "^2.1.0" - pgpass "1.x" - semver "4.3.2" - -pgpass@1.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" - integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= - dependencies: - split "^1.0.0" - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pkg-conf@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" - integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= - dependencies: - find-up "^2.0.0" - load-json-file "^4.0.0" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" - integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= - -postgres-date@~1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.6.tgz#4925e8085b30c2ba1a06ac91b9a3473954a2ce2d" - integrity sha512-o2a4gxeFcox+CgB3Ig/kNHBP23PiEXHCXx7pcIIsvzoNz4qv+lKTyiSkjOXIMNUl12MO/mOYl2K6wR9X5K6Plg== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.1: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -pupa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" - integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== - dependencies: - escape-goat "^2.0.0" - -qs@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.0.tgz#ed079be28682147e6fd9a34cc2b0c1e0ec6453ee" - integrity sha1-7Qeb4oaCFH5v2aNMwrDB4OxkU+4= - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@2.3.7, readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -registry-auth-token@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" - integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6, resolve@^1.1.7: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restler@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/restler/-/restler-3.4.0.tgz#741ec0b3d16b949feea2813d0c3c68529e888d9b" - integrity sha1-dB7As9FrlJ/uooE9DDxoUp6IjZs= - dependencies: - iconv-lite "0.2.11" - qs "1.2.0" - xml2js "0.4.0" - yaml "0.2.3" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -rxjs@^6.6.0: - version "6.6.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" - integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@0.5.x: - version "0.5.8" - resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" - integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -semver@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" - integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= - -semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -signale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/signale/-/signale-1.4.0.tgz#c4be58302fb0262ac00fc3d886a7c113759042f1" - integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== - dependencies: - chalk "^2.3.2" - figures "^2.0.0" - pkg-conf "^2.1.0" - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sqlite3@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.2.0.tgz#49026d665e9fc4f922e56fb9711ba5b4c85c4901" - integrity sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg== - dependencies: - nan "^2.12.1" - node-pre-gyp "^0.11.0" - -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE= - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-json-comments@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^5.3.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -tar@^4, tar@^4.4.2: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - -tarn@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tarn/-/tarn-2.0.0.tgz#c68499f69881f99ae955b4317ca7d212d942fdee" - integrity sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA== - -temp-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" - integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= - -temp-write@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" - integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== - dependencies: - graceful-fs "^4.1.15" - is-stream "^2.0.0" - make-dir "^3.0.0" - temp-dir "^1.0.0" - uuid "^3.3.2" - -term-size@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" - integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through@2, through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tildify@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" - integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= - -undefsafe@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" - integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== - dependencies: - debug "^2.2.0" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unix-timestamp@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/unix-timestamp/-/unix-timestamp-0.2.0.tgz#e1cdc2808df6327d27e635d9351e72815288733e" - integrity sha1-4c3CgI32Mn0n5jXZNR5ygVKIcz4= - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -update-notifier@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" - integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== - dependencies: - boxen "^4.2.0" - chalk "^3.0.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@^0.10.3: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== - dependencies: - inherits "2.0.3" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - -v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== - -v8flags@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.14, which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -xml2js@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.0.tgz#124fc4114b4129c810800ecb2ac86cf25462cb9a" - integrity sha1-Ek/EEUtBKcgQgA7LKshs8lRiy5o= - dependencies: - sax "0.5.x" - xmlbuilder ">=0.4.2" - -xmlbuilder@>=0.4.2: - version "15.1.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" - integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^3.0.0, yallist@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yaml@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-0.2.3.tgz#b5450e92e76ef36b5dd24e3660091ebaeef3e5c7" - integrity sha1-tUUOkudu82td0k42YAkeuu7z5cc= - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" diff --git a/docker/Dockerfile b/docker/Dockerfile index d85782b6..5ccdc947 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,54 +3,105 @@ # This file assumes that the frontend has been built using ./scripts/frontend-build -FROM jc21/nginx-full:node +#=============== +# gobuild +#=============== + +FROM jc21/nginx-full:github-acme.sh-golang AS gobuild + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG GOPROXY +ARG GOPRIVATE + +ENV GOPROXY=$GOPROXY \ + GOPRIVATE=$GOPRIVATE \ + GO111MODULE=on \ + CGO_ENABLED=1 + +# Nancy +RUN go get github.com/sonatype-nexus-community/nancy +RUN mkdir -p /workspace +WORKDIR /workspace +COPY backend/go.mod backend/go.sum backend/.nancy-ignore ./ +RUN go mod download + +ARG NANCY_TOKEN +ARG NANCY_USER +RUN go list -json -m all | nancy sleuth --quiet --username "${NANCY_USER}" --token "${NANCY_TOKEN}" +RUN rm -rf /workspace + +# Code +WORKDIR /app +COPY . . +WORKDIR /app/backend + +# Build +RUN go mod download +RUN echo "Testing and compiling project" \ + && [ -z "$(go tool fix -diff ./internal)" ] + +# Disabled as CI has issues at the moment +#RUN if [ "$TARGETPLATFORM" == "" ] || [ "$TARGETPLATFORM" == "linux/amd64" ]; then golangci-lint -v run ./...; fi + +RUN richgo test -cover -v ./internal/... +RUN richgo test -bench=. ./internal/... -ARG TARGETPLATFORM ARG BUILD_VERSION ARG BUILD_COMMIT -ARG BUILD_DATE +ARG SENTRY_DSN +RUN go build \ + -ldflags "-w -s -X main.commit=${BUILD_COMMIT} -X main.version=${BUILD_VERSION} -X main.sentryDSN=${SENTRY_DSN:-}" \ + -o ../dist/bin/server \ + -v ./cmd/server -ENV SUPPRESS_NO_CONFIG_WARNING=1 \ - S6_FIX_ATTRS_HIDDEN=1 \ - S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ - NODE_ENV=production \ - NPM_BUILD_VERSION="${BUILD_VERSION}" \ - NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ - NPM_BUILD_DATE="${BUILD_DATE}" +#=============== +# Final image +#=============== -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ - && apt-get update \ - && apt-get install -y --no-install-recommends jq \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +FROM jc21/nginx-full:github-acme.sh + +COPY --from=gobuild /app/dist /app +COPY --from=gobuild /app/backend/migrations /app/migrations +# COPY frontend/build /app/frontend + +ENV SUPPRESS_NO_CONFIG_WARNING=1 +ENV S6_FIX_ATTRS_HIDDEN=1 +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf # s6 overlay -COPY scripts/install-s6 /tmp/install-s6 -RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 +RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz" \ + && tar -xzf /tmp/s6-overlay-amd64.tar.gz -C / -EXPOSE 80 81 443 +EXPOSE 80/tcp 81/tcp 443/tcp -COPY backend /app -COPY frontend/dist /app/frontend -COPY global /app/global - -WORKDIR /app -RUN yarn install - -# add late to limit cache-busting by modifications COPY docker/rootfs / # Remove frontend service not required for prod, dev nginx config as well RUN rm -rf /etc/services.d/frontend /etc/nginx/conf.d/dev.conf -VOLUME [ "/data", "/etc/letsencrypt" ] -ENTRYPOINT [ "/init" ] -HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health +VOLUME /data + +CMD [ "/init" ] +HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://127.0.0.1:81/api || exit 1 + +ARG NOW +ARG BUILD_VERSION +ARG BUILD_COMMIT +ARG BUILD_DATE +ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}" +ENV DATABASE_URL="sqlite:////data/nginxproxymanager.db" \ + DBMATE_MIGRATIONS_DIR="/app/migrations" \ + DBMATE_SCHEMA_FILE="/data/schema.sql" \ + DBMATE_NO_DUMP_SCHEMA="1" LABEL org.label-schema.schema-version="1.0" \ org.label-schema.license="MIT" \ org.label-schema.name="nginx-proxy-manager" \ - org.label-schema.description="Docker container for managing Nginx proxy hosts with a simple, powerful interface " \ - org.label-schema.url="https://github.com/jc21/nginx-proxy-manager" \ + org.label-schema.description="Nginx Host Management and Proxy" \ + org.label-schema.build-date="$NOW" \ + org.label-schema.version="$BUILD_VERSION" \ + org.label-schema.url="https://nginxproxymanager.com" \ org.label-schema.vcs-url="https://github.com/jc21/nginx-proxy-manager.git" \ - org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:latest" + org.label-schema.vcs-ref="$BUILD_COMMIT" \ + org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:$BUILD_VERSION" diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index ae17e861..f868356a 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,15 +1,30 @@ -FROM jc21/nginx-full:node +FROM jc21/nginx-full:github-acme.sh-golang LABEL maintainer="Jamie Curnow " -ENV S6_LOGGING=0 \ - SUPPRESS_NO_CONFIG_WARNING=1 \ - S6_FIX_ATTRS_HIDDEN=1 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ +ARG GOPROXY +ARG GOPRIVATE + +ENV GOPROXY=$GOPROXY \ + GOPRIVATE=$GOPRIVATE \ + S6_LOGGING=0 \ + SUPPRESS_NO_CONFIG_WARNING=1 \ + S6_FIX_ATTRS_HIDDEN=1 \ + DATABASE_URL="sqlite:////data/nginxproxymanager.db" \ + DBMATE_MIGRATIONS_DIR="/app/backend/migrations" \ + DBMATE_SCHEMA_FILE="/data/schema.sql" + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf + +# Sqlite client and litecli client, and node +RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \ && apt-get update \ - && apt-get install -y certbot jq python3-pip \ + && apt-get install -y --no-install-recommends nodejs \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && pip install -U litecli \ + && npm install -g yarn # Task RUN cd /usr \ @@ -23,6 +38,9 @@ RUN rm -f /etc/nginx/conf.d/production.conf RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz" \ && tar -xzf /tmp/s6-overlay-amd64.tar.gz -C / -EXPOSE 80 81 443 -ENTRYPOINT [ "/init" ] -HEALTHCHECK --interval=5s --timeout=3s CMD /bin/check-health +# Fix for golang dev: +RUN chown -R 1000:1000 /opt/go + +EXPOSE 80 +CMD [ "/init" ] +HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://127.0.0.1:81/api || exit 1 diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 771b8299..9412f302 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -2,71 +2,25 @@ version: "3" services: - fullstack-mysql: + fullstack: image: ${IMAGE}:ci-${BUILD_NUMBER} environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" + - LOG_LEVEL=debug volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 - depends_on: - - db + - npm_data_ci:/data + - ../docs:/temp-docs - fullstack-sqlite: - image: ${IMAGE}:ci-${BUILD_NUMBER} - environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DB_SQLITE_FILE: "/data/database.sqlite" - volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 - - db: - image: jc21/mariadb-aria - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" - volumes: - - db_data:/var/lib/mysql - - cypress-mysql: + cypress: image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} build: - context: ../test/ - dockerfile: cypress/Dockerfile + context: ../ + dockerfile: test/cypress/Dockerfile environment: - CYPRESS_baseUrl: "http://fullstack-mysql:81" - volumes: - - cypress-logs:/results - command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} - - cypress-sqlite: - image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} - build: - context: ../test/ - dockerfile: cypress/Dockerfile - environment: - CYPRESS_baseUrl: "http://fullstack-sqlite:81" + CYPRESS_baseUrl: "http://fullstack:81" volumes: - cypress-logs:/results command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} volumes: cypress-logs: - npm_data: - db_data: + npm_data_ci: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a0b4547b..25c60dd7 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,4 +1,4 @@ -# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. +# WARNING: This is a DEVELOPMENT docker-compose file used for development of the entire app, it should not be used for production. version: "3" services: @@ -11,57 +11,25 @@ services: - 3080:80 - 3081:81 - 3443:443 - networks: - - nginx_proxy_manager environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DEVELOPMENT: "true" - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" - # DB_SQLITE_FILE: "/data/database.sqlite" - # DISABLE_IPV6: "true" + - DEVELOPMENT=true + - GOPROXY=${GOPROXY:-} + - GOPRIVATE=${GOPRIVATE:-} + - LOG_LEVEL=debug + - PUID=1000 + - PGID=1000 volumes: - - npm_data:/data - - le_data:/etc/letsencrypt - - ../backend:/app - - ../frontend:/app/frontend - - ../global:/app/global - depends_on: - - db + - ../:/app + - ./rootfs/var/www/html:/var/www/html + - ../data:/data working_dir: /app - db: - image: jc21/mariadb-aria - networks: - - nginx_proxy_manager - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" - volumes: - - db_data:/var/lib/mysql - swagger: image: 'swaggerapi/swagger-ui:latest' ports: - 3001:80 - networks: - - nginx_proxy_manager environment: - URL: "http://127.0.0.1:3081/api/schema" + URL: "http://${SWAGGER_PUBLIC_DOMAIN:-127.0.0.1:3081}/api/schema" PORT: '80' depends_on: - npm - -volumes: - npm_data: - le_data: - db_data: - -networks: - nginx_proxy_manager: diff --git a/docker/rootfs/bin/check-health b/docker/rootfs/bin/check-health deleted file mode 100755 index bcf5552b..00000000 --- a/docker/rootfs/bin/check-health +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -OK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status') - -if [ "$OK" == "OK" ]; then - echo "OK" - exit 0 -else - echo "NOT OK" - exit 1 -fi diff --git a/docker/rootfs/bin/handle-ipv6-setting b/docker/rootfs/bin/handle-ipv6-setting deleted file mode 100755 index 2aa0e41a..00000000 --- a/docker/rootfs/bin/handle-ipv6-setting +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# This command reads the `DISABLE_IPV6` env var and will either enable -# or disable ipv6 in all nginx configs based on this setting. - -# Lowercase -DISABLE_IPV6=$(echo "${DISABLE_IPV6:-}" | tr '[:upper:]' '[:lower:]') - -CYAN='\E[1;36m' -BLUE='\E[1;34m' -YELLOW='\E[1;33m' -RED='\E[1;31m' -RESET='\E[0m' - -FOLDER=$1 -if [ "$FOLDER" == "" ]; then - echo -e "${RED}❯ $0 requires a absolute folder path as the first argument!${RESET}" - echo -e "${YELLOW} ie: $0 /data/nginx${RESET}" - exit 1 -fi - -FILES=$(find "$FOLDER" -type f -name "*.conf") -if [ "$DISABLE_IPV6" == "true" ] || [ "$DISABLE_IPV6" == "on" ] || [ "$DISABLE_IPV6" == "1" ] || [ "$DISABLE_IPV6" == "yes" ]; then - # IPV6 is disabled - echo "Disabling IPV6 in hosts" - echo -e "${BLUE}❯ ${CYAN}Disabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}" - - # Iterate over configs and run the regex - for FILE in $FILES - do - echo -e " ${BLUE}❯ ${YELLOW}${FILE}${RESET}" - sed -E -i 's/^([^#]*)listen \[::\]/\1#listen [::]/g' "$FILE" - done - -else - # IPV6 is enabled - echo -e "${BLUE}❯ ${CYAN}Enabling IPV6 in hosts: ${YELLOW}${FOLDER}${RESET}" - - # Iterate over configs and run the regex - for FILE in $FILES - do - echo -e " ${BLUE}❯ ${YELLOW}${FILE}${RESET}" - sed -E -i 's/^(\s*)#listen \[::\]/\1listen [::]/g' "$FILE" - done - -fi diff --git a/docker/rootfs/etc/cont-init.d/.gitignore b/docker/rootfs/etc/cont-init.d/.gitignore deleted file mode 100644 index f04f0f6e..00000000 --- a/docker/rootfs/etc/cont-init.d/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!*.sh diff --git a/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh b/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh deleted file mode 100644 index f145807a..00000000 --- a/docker/rootfs/etc/cont-init.d/01_s6-secret-init.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/with-contenv bash -# ref: https://github.com/linuxserver/docker-baseimage-alpine/blob/master/root/etc/cont-init.d/01-envfile - -# in s6, environmental variables are written as text files for s6 to monitor -# seach through full-path filenames for files ending in "__FILE" -for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do - echo "[secret-init] Evaluating ${FILENAME##*/} ..." - - # set SECRETFILE to the contents of the full-path textfile - SECRETFILE=$(cat ${FILENAME}) - # SECRETFILE=${FILENAME} - # echo "[secret-init] Set SECRETFILE to ${SECRETFILE}" # DEBUG - rm for prod! - - # if SECRETFILE exists / is not null - if [[ -f ${SECRETFILE} ]]; then - # strip the appended "__FILE" from environmental variable name ... - STRIPFILE=$(echo ${FILENAME} | sed "s/__FILE//g") - # echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod! - - # ... and set value to contents of secretfile - # since s6 uses text files, this is effectively "export ..." - printf $(cat ${SECRETFILE}) > ${STRIPFILE} - # echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!" - echo "[secret-init] Success! ${STRIPFILE##*/} set from ${FILENAME##*/}" - - else - echo "[secret-init] cannot find secret in ${FILENAME}" - fi -done diff --git a/docker/rootfs/etc/cont-init.d/10-nginx b/docker/rootfs/etc/cont-init.d/10-nginx new file mode 100755 index 00000000..4a3b38be --- /dev/null +++ b/docker/rootfs/etc/cont-init.d/10-nginx @@ -0,0 +1,44 @@ +#!/usr/bin/with-contenv bash + +# Create required folders +mkdir -p /tmp/nginx/body \ + /run/nginx \ + /var/log/nginx \ + /data/nginx \ + /data/custom_ssl \ + /data/logs \ + /data/access \ + /data/nginx/default_host \ + /data/nginx/default_www \ + /data/nginx/proxy_host \ + /data/nginx/redirection_host \ + /data/nginx/stream \ + /data/nginx/dead_host \ + /data/nginx/temp \ + /var/lib/nginx/cache/public \ + /var/lib/nginx/cache/private \ + /var/cache/nginx/proxy_temp \ + /data/acme.sh + +touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log && chmod -R 777 /var/cache/nginx + +# Dynamically generate resolvers file +echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf)" ";" > /etc/nginx/conf.d/include/resolvers.conf + +# Generate dummy self-signed certificate. +if [ ! -f /data/nginx/dummycert.pem ] || [ ! -f /data/nginx/dummykey.pem ] +then + echo "Generating dummy SSL certificate..." + openssl req \ + -new \ + -newkey rsa:2048 \ + -days 3650 \ + -nodes \ + -x509 \ + -subj '/O=Nginx Proxy Manager/OU=Dummy Certificate/CN=localhost' \ + -keyout /data/nginx/dummykey.pem \ + -out /data/nginx/dummycert.pem + echo "Complete" +else + echo "Skipping generation of dummy SSL cert" +fi diff --git a/docker/rootfs/etc/cont-init.d/20-adduser b/docker/rootfs/etc/cont-init.d/20-adduser new file mode 100755 index 00000000..31340800 --- /dev/null +++ b/docker/rootfs/etc/cont-init.d/20-adduser @@ -0,0 +1,33 @@ +#!/usr/bin/with-contenv bash + +PUID=${PUID:-911} +PGID=${PGID:-911} + +groupmod -g 1000 users || exit 1 +useradd -u "${PUID}" -U -d /data -s /bin/false npmuser || exit 1 +usermod -G users npmuser || exit 1 +groupmod -o -g "$PGID" npmuser || exit 1 + +echo "------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +User UID: $(id -u npmuser) +User GID: $(id -g npmuser) +------------------------------------- +" + +chown -R npmuser:npmuser /data +chown -R npmuser:npmuser /run/nginx +chown -R npmuser:npmuser /etc/nginx +chown -R npmuser:npmuser /tmp/nginx +chown -R npmuser:npmuser /var/cache/nginx +chown -R npmuser:npmuser /var/lib/nginx +chown -R npmuser:npmuser /var/log/nginx + +# Home for npmuser +mkdir -p /tmp/npmuserhome +chown -R npmuser:npmuser /tmp/npmuserhome diff --git a/docker/rootfs/etc/cont-init.d/30-dbmate b/docker/rootfs/etc/cont-init.d/30-dbmate new file mode 100755 index 00000000..bf68358b --- /dev/null +++ b/docker/rootfs/etc/cont-init.d/30-dbmate @@ -0,0 +1,16 @@ +#!/usr/bin/with-contenv bash + +CYAN='\E[1;36m' +YELLOW='\E[1;33m' +MAGENTA='\E[1;35m' +RESET='\E[0m' + +if [ "$LOG_LEVEL" == "debug" ]; then + echo -e "${MAGENTA}[DEBUG] ${CYAN}DATABASE_URL=${YELLOW}${DATABASE_URL}${RESET}" +fi + +# Firstly create the sqlite database if it doesn't already exist +# and run any migrations required +echo -e "${YELLOW}Running dbmate migrations ...${RESET}" +s6-setuidgid npmuser /bin/dbmate up || exit 1 +echo -e "${GREEN}Completed dbmate migrations!${RESET}" diff --git a/docker/rootfs/etc/letsencrypt.ini b/docker/rootfs/etc/letsencrypt.ini deleted file mode 100644 index 3565d6e5..00000000 --- a/docker/rootfs/etc/letsencrypt.ini +++ /dev/null @@ -1,4 +0,0 @@ -text = True -non-interactive = True -authenticator = webroot -webroot-path = /data/letsencrypt-acme-challenge diff --git a/docker/rootfs/etc/nginx/conf.d/default.conf b/docker/rootfs/etc/nginx/conf.d/default.conf index d1684ea7..5d5eedf9 100644 --- a/docker/rootfs/etc/nginx/conf.d/default.conf +++ b/docker/rootfs/etc/nginx/conf.d/default.conf @@ -1,17 +1,9 @@ # "You are not configured" page, which is the default if another default doesn't exist server { listen 80; - listen [::]:80; - - set $forward_scheme "http"; - set $server "127.0.0.1"; - set $port "80"; - - server_name localhost-nginx-proxy-manager; - access_log /data/logs/default.log standard; - error_log /dev/null crit; - include conf.d/include/assets.conf; + server_name localhost; include conf.d/include/block-exploits.conf; + access_log /data/logs/default.log proxy; location / { index index.html; @@ -22,15 +14,10 @@ server { # First 443 Host, which is the default if another default doesn't exist server { listen 443 ssl; - listen [::]:443 ssl; - - set $forward_scheme "https"; - set $server "127.0.0.1"; - set $port "443"; - server_name localhost; - access_log /data/logs/default.log standard; - error_log /dev/null crit; + include conf.d/include/block-exploits.conf; + access_log /data/logs/default.log proxy; + ssl_certificate /data/nginx/dummycert.pem; ssl_certificate_key /data/nginx/dummykey.pem; include conf.d/include/ssl-ciphers.conf; diff --git a/docker/rootfs/etc/nginx/conf.d/dev.conf b/docker/rootfs/etc/nginx/conf.d/dev.conf index edbdec8a..ce8c1da7 100644 --- a/docker/rootfs/etc/nginx/conf.d/dev.conf +++ b/docker/rootfs/etc/nginx/conf.d/dev.conf @@ -1,10 +1,6 @@ server { listen 81 default; - listen [::]:81 default; - server_name nginxproxymanager-dev; - root /app/frontend/dist; - access_log /dev/null; location /api { return 302 /api/; @@ -16,14 +12,22 @@ server { proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; - proxy_pass http://127.0.0.1:3000/; + proxy_pass http://127.0.0.1:3000/api/; + } - proxy_read_timeout 15m; - proxy_send_timeout 15m; + location ~ .html { + try_files $uri =404; } location / { - index index.html; - try_files $uri $uri.html $uri/ /index.html; + add_header X-Served-By $host; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass http://127.0.0.1:9000; } } diff --git a/docker/rootfs/etc/nginx/conf.d/include/.gitignore b/docker/rootfs/etc/nginx/conf.d/include/.gitignore deleted file mode 100644 index 5291fe15..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/.gitignore +++ /dev/null @@ -1 +0,0 @@ -resolvers.conf diff --git a/docker/rootfs/etc/nginx/conf.d/include/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf index e95c2e8b..7dd0f5ce 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/assets.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/assets.conf @@ -1,31 +1,31 @@ location ~* ^.*\.(css|js|jpe?g|gif|png|woff|eot|ttf|svg|ico|css\.map|js\.map)$ { - if_modified_since off; + if_modified_since off; - # use the public cache - proxy_cache public-cache; - proxy_cache_key $host$request_uri; + # use the public cache + proxy_cache public-cache; + proxy_cache_key $host$request_uri; - # ignore these headers for media - proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; + # ignore these headers for media + proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; - # cache 200s and also 404s (not ideal but there are a few 404 images for some reason) - proxy_cache_valid any 30m; - proxy_cache_valid 404 1m; + # cache 200s and also 404s (not ideal but there are a few 404 images for some reason) + proxy_cache_valid any 30m; + proxy_cache_valid 404 1m; - # strip this header to avoid If-Modified-Since requests - proxy_hide_header Last-Modified; - proxy_hide_header Cache-Control; - proxy_hide_header Vary; + # strip this header to avoid If-Modified-Since requests + proxy_hide_header Last-Modified; + proxy_hide_header Cache-Control; + proxy_hide_header Vary; - proxy_cache_bypass 0; - proxy_no_cache 0; + proxy_cache_bypass 0; + proxy_no_cache 0; - proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404; - proxy_connect_timeout 5s; - proxy_read_timeout 45s; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404; + proxy_connect_timeout 5s; + proxy_read_timeout 45s; - expires @30m; - access_log off; + expires @30m; + access_log off; - include conf.d/include/proxy.conf; + include conf.d/include/proxy.conf; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf index 093bda23..22360fc1 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf @@ -2,92 +2,92 @@ set $block_sql_injections 0; if ($query_string ~ "union.*select.*\(") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($query_string ~ "union.*all.*select.*") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($query_string ~ "concat.*\(") { - set $block_sql_injections 1; + set $block_sql_injections 1; } if ($block_sql_injections = 1) { - return 403; + return 403; } ## Block file injections set $block_file_injections 0; if ($query_string ~ "[a-zA-Z0-9_]=http://") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { - set $block_file_injections 1; + set $block_file_injections 1; } if ($block_file_injections = 1) { - return 403; + return 403; } ## Block common exploits set $block_common_exploits 0; if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "proc/self/environ") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($query_string ~ "base64_(en|de)code\(.*\)") { - set $block_common_exploits 1; + set $block_common_exploits 1; } if ($block_common_exploits = 1) { - return 403; + return 403; } ## Block spam set $block_spam 0; if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") { - set $block_spam 1; + set $block_spam 1; } if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") { - set $block_spam 1; + set $block_spam 1; } if ($block_spam = 1) { - return 403; + return 403; } ## Block user agents @@ -95,42 +95,42 @@ set $block_user_agents 0; # Disable Akeeba Remote Control 2.5 and earlier if ($http_user_agent ~ "Indy Library") { - set $block_user_agents 1; + set $block_user_agents 1; } # Common bandwidth hoggers and hacking tools. if ($http_user_agent ~ "libwww-perl") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GetRight") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GetWeb!") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Go!Zilla") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Download Demon") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "Go-Ahead-Got-It") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "TurnitinBot") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($http_user_agent ~ "GrabNet") { - set $block_user_agents 1; + set $block_user_agents 1; } if ($block_user_agents = 1) { - return 403; + return 403; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf index 15f0d285..5fd4810f 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf @@ -1,3 +1,3 @@ if ($scheme = "http") { - return 301 https://$host$request_uri; + return 301 https://$host$request_uri; } diff --git a/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf b/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf deleted file mode 100644 index 34249325..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf +++ /dev/null @@ -1,2 +0,0 @@ -# This should be left blank is it is populated programatically -# by the application backend. diff --git a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf b/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf deleted file mode 100644 index d04f7033..00000000 --- a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf +++ /dev/null @@ -1,29 +0,0 @@ -# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) -# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel -# other regex checks, because in our other config files have regex rule that denies access to files with dotted names. -location ^~ /.well-known/acme-challenge/ { - # Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure - # we need to open up access by turning off auth and IP ACL for this location. - auth_basic off; - allow all; - - # Set correct content type. According to this: - # https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29 - # Current specification requires "text/plain" or no content header at all. - # It seems that "text/plain" is a safe option. - default_type "text/plain"; - - # This directory must be the same as in /etc/letsencrypt/cli.ini - # as "webroot-path" parameter. Also don't forget to set "authenticator" parameter - # there to "webroot". - # Do NOT use alias, use root! Target directory is located here: - # /var/www/common/letsencrypt/.well-known/acme-challenge/ - root /data/letsencrypt-acme-challenge; -} - -# Hide /acme-challenge subdirectory and return 404 on all requests. -# It is somewhat more secure than letting Nginx return 403. -# Ending slash is important! -location = /.well-known/acme-challenge/ { - return 404; -} diff --git a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf index c0dce061..b84a4513 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf @@ -3,6 +3,4 @@ proxy_set_header Host $host; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; -proxy_set_header X-Real-IP $remote_addr; proxy_pass $forward_scheme://$server:$port; - diff --git a/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf b/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf new file mode 100644 index 00000000..ccd9dcef --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/resolvers.conf @@ -0,0 +1 @@ +# Intentionally blank diff --git a/docker/rootfs/etc/nginx/conf.d/production.conf b/docker/rootfs/etc/nginx/conf.d/production.conf index 877e51dd..325cb8cc 100644 --- a/docker/rootfs/etc/nginx/conf.d/production.conf +++ b/docker/rootfs/etc/nginx/conf.d/production.conf @@ -1,33 +1,14 @@ # Admin Interface server { listen 81 default; - listen [::]:81 default; - server_name nginxproxymanager; - root /app/frontend; - access_log /dev/null; - location /api { - return 302 /api/; - } - - location /api/ { + location / { add_header X-Served-By $host; proxy_set_header Host $host; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; - proxy_pass http://127.0.0.1:3000/; - - proxy_read_timeout 15m; - proxy_send_timeout 15m; - } - - location / { - index index.html; - if ($request_uri ~ ^/(.*)\.html$) { - return 302 /$1; - } - try_files $uri $uri.html $uri/ /index.html; + proxy_pass http://localhost:3000/; } } diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 40432968..aac80c3c 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -1,7 +1,6 @@ # run nginx in foreground daemon off; - -user root; +pid /run/nginx/nginx.pid; # Set number of worker processes automatically based on number of CPU cores. worker_processes auto; @@ -26,15 +25,12 @@ http { tcp_nopush on; tcp_nodelay on; client_body_temp_path /tmp/nginx/body 1 2; - keepalive_timeout 90s; - proxy_connect_timeout 90s; - proxy_send_timeout 90s; - proxy_read_timeout 90s; + keepalive_timeout 65; ssl_prefer_server_ciphers on; gzip on; proxy_ignore_client_abort off; - client_max_body_size 2000m; - server_names_hash_bucket_size 1024; + client_max_body_size 200m; + server_names_hash_bucket_size 64; proxy_http_version 1.1; proxy_set_header X-Forwarded-Scheme $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -43,54 +39,27 @@ http { proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; - log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; - log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; - - + log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; access_log /data/logs/default.log proxy; - # Dynamically generated resolvers file - include /etc/nginx/conf.d/include/resolvers.conf; - # Default upstream scheme map $host $forward_scheme { default http; } # Real IP Determination - - # Local subnets: - set_real_ip_from 10.0.0.0/8; - set_real_ip_from 172.16.0.0/12; # Includes Docker subnet - set_real_ip_from 192.168.0.0/16; + # Docker subnet: + set_real_ip_from 172.0.0.0/8; # NPM generated CDN ip ranges: - include conf.d/include/ip_ranges.conf; + #include conf.d/include/ip_ranges.conf; # always put the following 2 lines after ip subnets: - real_ip_header X-Real-IP; + real_ip_header X-Forwarded-For; real_ip_recursive on; - # Custom - include /data/nginx/custom/http_top[.]conf; - # Files generated by NPM include /etc/nginx/conf.d/*.conf; include /data/nginx/default_host/*.conf; include /data/nginx/proxy_host/*.conf; include /data/nginx/redirection_host/*.conf; include /data/nginx/dead_host/*.conf; - include /data/nginx/temp/*.conf; - - # Custom - include /data/nginx/custom/http[.]conf; } - -stream { - # Files generated by NPM - include /data/nginx/stream/*.conf; - - # Custom - include /data/nginx/custom/stream[.]conf; -} - -# Custom -include /data/nginx/custom/root[.]conf; diff --git a/docker/rootfs/etc/services.d/backend/finish b/docker/rootfs/etc/services.d/backend/finish new file mode 100755 index 00000000..2b661f61 --- /dev/null +++ b/docker/rootfs/etc/services.d/backend/finish @@ -0,0 +1,5 @@ +#!/usr/bin/execlineb -S1 +if { s6-test ${1} -ne 0 } +if { s6-test ${1} -ne 256 } + +s6-svscanctl -t /var/run/s6/services diff --git a/docker/rootfs/etc/services.d/backend/run b/docker/rootfs/etc/services.d/backend/run new file mode 100755 index 00000000..b63ad826 --- /dev/null +++ b/docker/rootfs/etc/services.d/backend/run @@ -0,0 +1,23 @@ +#!/usr/bin/with-contenv bash + +RESET='\E[0m' +YELLOW='\E[1;33m' + +echo -e "${YELLOW}Starting backend API ...${RESET}" + +if [ "$DEVELOPMENT" == "true" ]; then + HOME=/tmp/npmuserhome + GOPATH="$HOME/go" + mkdir -p "$GOPATH" + chown -R npmuser:npmuser "$GOPATH" + export HOME GOPATH + cd /app/backend || exit 1 + s6-setuidgid npmuser task -w +else + cd /app/bin || exit 1 + while : + do + s6-setuidgid npmuser /app/bin/server + sleep 1 + done +fi diff --git a/docker/rootfs/etc/services.d/frontend/finish b/docker/rootfs/etc/services.d/frontend/finish index bca9a35d..2b661f61 100755 --- a/docker/rootfs/etc/services.d/frontend/finish +++ b/docker/rootfs/etc/services.d/frontend/finish @@ -3,4 +3,3 @@ if { s6-test ${1} -ne 0 } if { s6-test ${1} -ne 256 } s6-svscanctl -t /var/run/s6/services - diff --git a/docker/rootfs/etc/services.d/frontend/run b/docker/rootfs/etc/services.d/frontend/run index 32558d98..87963721 100755 --- a/docker/rootfs/etc/services.d/frontend/run +++ b/docker/rootfs/etc/services.d/frontend/run @@ -3,9 +3,13 @@ # This service is DEVELOPMENT only. if [ "$DEVELOPMENT" == "true" ]; then + CI=true + HOME=/tmp/npmuserhome + export CI + export HOME cd /app/frontend || exit 1 - yarn install - yarn watch + s6-setuidgid npmuser yarn install + s6-setuidgid npmuser yarn start else exit 0 fi diff --git a/docker/rootfs/etc/services.d/manager/finish b/docker/rootfs/etc/services.d/manager/finish deleted file mode 100755 index 7d442d6a..00000000 --- a/docker/rootfs/etc/services.d/manager/finish +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/with-contenv bash - -s6-svscanctl -t /var/run/s6/services diff --git a/docker/rootfs/etc/services.d/manager/run b/docker/rootfs/etc/services.d/manager/run deleted file mode 100755 index ba0fb05e..00000000 --- a/docker/rootfs/etc/services.d/manager/run +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/with-contenv bash - -mkdir -p /data/letsencrypt-acme-challenge - -cd /app || echo - -if [ "$DEVELOPMENT" == "true" ]; then - cd /app || exit 1 - yarn install - node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js -else - cd /app || exit 1 - while : - do - node --abort_on_uncaught_exception --max_old_space_size=250 index.js - sleep 1 - done -fi diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/rootfs/etc/services.d/nginx/finish deleted file mode 120000 index 63b10de4..00000000 --- a/docker/rootfs/etc/services.d/nginx/finish +++ /dev/null @@ -1 +0,0 @@ -/bin/true \ No newline at end of file diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/rootfs/etc/services.d/nginx/finish new file mode 100755 index 00000000..bca9a35d --- /dev/null +++ b/docker/rootfs/etc/services.d/nginx/finish @@ -0,0 +1,6 @@ +#!/usr/bin/execlineb -S1 +if { s6-test ${1} -ne 0 } +if { s6-test ${1} -ne 256 } + +s6-svscanctl -t /var/run/s6/services + diff --git a/docker/rootfs/etc/services.d/nginx/run b/docker/rootfs/etc/services.d/nginx/run index 2941db40..e04643e7 100755 --- a/docker/rootfs/etc/services.d/nginx/run +++ b/docker/rootfs/etc/services.d/nginx/run @@ -1,49 +1,3 @@ #!/usr/bin/with-contenv bash -# Create required folders -mkdir -p /tmp/nginx/body \ - /run/nginx \ - /var/log/nginx \ - /data/nginx \ - /data/custom_ssl \ - /data/logs \ - /data/access \ - /data/nginx/default_host \ - /data/nginx/default_www \ - /data/nginx/proxy_host \ - /data/nginx/redirection_host \ - /data/nginx/stream \ - /data/nginx/dead_host \ - /data/nginx/temp \ - /var/lib/nginx/cache/public \ - /var/lib/nginx/cache/private \ - /var/cache/nginx/proxy_temp - -touch /var/log/nginx/error.log && chmod 777 /var/log/nginx/error.log && chmod -R 777 /var/cache/nginx -chown root /tmp/nginx - -# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` -# thanks @tfmm -echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" {print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf);" > /etc/nginx/conf.d/include/resolvers.conf - -# Generate dummy self-signed certificate. -if [ ! -f /data/nginx/dummycert.pem ] || [ ! -f /data/nginx/dummykey.pem ] -then - echo "Generating dummy SSL certificate..." - openssl req \ - -new \ - -newkey rsa:2048 \ - -days 3650 \ - -nodes \ - -x509 \ - -subj '/O=Nginx Proxy Manager/OU=Dummy Certificate/CN=localhost' \ - -keyout /data/nginx/dummykey.pem \ - -out /data/nginx/dummycert.pem - echo "Complete" -fi - -# Handle IPV6 settings -/bin/handle-ipv6-setting /etc/nginx/conf.d -/bin/handle-ipv6-setting /data/nginx - -exec nginx +exec s6-setuidgid npmuser nginx diff --git a/docker/rootfs/root/.bashrc b/docker/rootfs/root/.bashrc index 1deb975c..122da279 100644 --- a/docker/rootfs/root/.bashrc +++ b/docker/rootfs/root/.bashrc @@ -16,7 +16,5 @@ alias h='cd ~;clear;' echo -e -n '\E[1;34m' figlet -w 120 "NginxProxyManager" -echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, ${ID:-debian} \E[1;32m${VERSION:-unknown}\E[1;36m, Certbot \E[1;32m$(certbot --version)\E[0m" -echo -e -n '\E[1;34m' -cat /built-for-arch -echo -e '\E[0m' +echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-3.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, Debian \E[1;32m${VERSION_ID:-unknown}\E[1;36m, Kernel \E[1;32m$(uname -r)\E[0m" +echo diff --git a/docker/rootfs/root/.config/litecli/config b/docker/rootfs/root/.config/litecli/config new file mode 100644 index 00000000..bfc67aa8 --- /dev/null +++ b/docker/rootfs/root/.config/litecli/config @@ -0,0 +1,13 @@ +[main] +multi_line = True +log_level = INFO +table_format = psql +syntax_style = monokai +wider_completion_menu = True +prompt = '\d> ' +prompt_continuation = '-> ' +less_chatty = True +auto_vertical_output = True + +[favorite_queries] +show_users = select * from user diff --git a/docker/rootfs/var/www/html/fonts/Aldrich-Regular.ttf b/docker/rootfs/var/www/html/fonts/Aldrich-Regular.ttf new file mode 100644 index 00000000..5c15ab92 Binary files /dev/null and b/docker/rootfs/var/www/html/fonts/Aldrich-Regular.ttf differ diff --git a/docker/rootfs/var/www/html/fonts/Poppins-Bold.ttf b/docker/rootfs/var/www/html/fonts/Poppins-Bold.ttf new file mode 100644 index 00000000..44313ca4 Binary files /dev/null and b/docker/rootfs/var/www/html/fonts/Poppins-Bold.ttf differ diff --git a/docker/rootfs/var/www/html/fonts/Poppins-Regular.ttf b/docker/rootfs/var/www/html/fonts/Poppins-Regular.ttf new file mode 100644 index 00000000..246a861a Binary files /dev/null and b/docker/rootfs/var/www/html/fonts/Poppins-Regular.ttf differ diff --git a/docker/rootfs/var/www/html/images/bg.jpg b/docker/rootfs/var/www/html/images/bg.jpg new file mode 100644 index 00000000..ccdca136 Binary files /dev/null and b/docker/rootfs/var/www/html/images/bg.jpg differ diff --git a/docker/rootfs/var/www/html/images/logo.png b/docker/rootfs/var/www/html/images/logo.png new file mode 100644 index 00000000..d91fff44 Binary files /dev/null and b/docker/rootfs/var/www/html/images/logo.png differ diff --git a/docker/rootfs/var/www/html/index.html b/docker/rootfs/var/www/html/index.html index 8478b47f..92c1d8fa 100644 --- a/docker/rootfs/var/www/html/index.html +++ b/docker/rootfs/var/www/html/index.html @@ -1,24 +1,30 @@ - - - - - Default Site - - - - -
-
-

Congratulations!

-

You've successfully started the Nginx Proxy Manager.

-

If you're seeing this site then you're trying to access a host that isn't set up yet.

-

Log in to the Admin panel to get started.

-
-

Powered by Nginx Proxy Manager

-
- + + Site not found :: Nginx Proxy Manager + + + + + + + +
+
+
+ Logo +
+
+ + +
+
+
+
+

This site is

+

not yet configured

+
+
+
+ diff --git a/docker/rootfs/var/www/html/main.css b/docker/rootfs/var/www/html/main.css new file mode 100644 index 00000000..e55b7ae0 --- /dev/null +++ b/docker/rootfs/var/www/html/main.css @@ -0,0 +1,399 @@ +*, *:before, *:after { + margin: 0px; + padding: 0px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body, html { + font-family: Arial, sans-serif; + font-size: 15px; + color: #666666; + + height: 100%; + background-color: #fff; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +a:focus {outline: none;} +a:hover {text-decoration: none;} + +h1,h2,h3,h4,h5,h6,p {margin: 0px;} + +ul, li { + margin: 0px; + list-style-type: none; +} + +.logo-img { + border: 2px solid #3c99b9; + border-radius: 50%; +} + +.p-t-24 {padding-top: 24px;} +.p-t-34 {padding-top: 34px;} +.p-b-10 {padding-bottom: 10px;} +.p-b-45 {padding-bottom: 45px;} +.p-b-60 {padding-bottom: 60px;} +.p-b-175 {padding-bottom: 175px;} +.p-l-80 {padding-left: 80px;} +.p-r-74 {padding-right: 74px;} +.p-r-200 {padding-right: 200px;} +.m-t-10 {margin-top: 10px;} +.m-b-10 {margin-bottom: 10px;} +.m-r-6 {margin-right: 6px;} +.m-r-30 {margin-right: 30px;} + +.bo-cir {border-radius: 50%;} +.of-hidden {overflow: hidden;} +.visible-false {visibility: hidden;} +.visible-true {visibility: visible;} + +.trans-04 { + -webkit-transition: all 0.4s; + -o-transition: all 0.4s; + -moz-transition: all 0.4s; + transition: all 0.4s; +} + +.flex-w, +.flex-sa, +.flex-c-m, +.flex-sb-m, +.dis-flex { + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; +} + +.flex-w { + -webkit-flex-wrap: wrap; + -moz-flex-wrap: wrap; + -ms-flex-wrap: wrap; + -o-flex-wrap: wrap; + flex-wrap: wrap; +} + +.flex-sa { + justify-content: space-around; +} + +.flex-c-m { + justify-content: center; + -ms-align-items: center; + align-items: center; +} + +.flex-sb-m { + justify-content: space-between; + -ms-align-items: center; + align-items: center; +} + +.flex-row { + -webkit-flex-direction: row; + -moz-flex-direction: row; + -ms-flex-direction: row; + -o-flex-direction: row; + flex-direction: row; +} + +@font-face { + font-family: Poppins-Regular; + src: url('fonts/Poppins-Regular.ttf'); +} + +@font-face { + font-family: Poppins-Bold; + src: url('fonts/Poppins-Bold.ttf'); +} + +@font-face { + font-family: Aldrich-Regular; + src: url('fonts/Aldrich-Regular.ttf'); +} + +.container {max-width: 1200px;} + +.cl0 {color: #fff;} +.s1-txt1 { + font-family: Poppins-Bold; + font-size: 15px; + color: #555; + line-height: 1.2; +} + +.s1-txt2 { + font-family: Poppins-Bold; + font-size: 15px; + color: #fff; + line-height: 1.2; +} + +.s1-txt3 { + font-family: Poppins-Regular; + font-size: 13px; + color: #999; + line-height: 1.5; +} + +.l1-txt1 { + font-family: Poppins-Regular; + font-size: 30px; + color: #fff; + line-height: 1.2; + text-transform: uppercase; +} + +.l1-txt2 { + font-family: Poppins-Bold; + font-size: 70px; + color: #fff; + line-height: 1.1; + text-transform: uppercase; +} + + .l1-txt3 { + font-family: Poppins-Bold; + font-size: 30px; + color: #333; + line-height: 1.2; + text-transform: uppercase; + } + + .size1 { + width: 100%; + min-height: 100vh; + } + + .size2 { + width: 100%; + height: 50px; + } + + .size3 { + width: 36px; + height: 36px; + } + + .wsize1 { + width: 390px; + max-width: 100%; + } + + .bg0 {background-color: #fff;} + + .bg-img1 { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + } + + .bor1 { + border-radius: 10px; + } + + .overlay1 { + position: relative; + z-index: 1; + } + .overlay1::before { + content: ""; + display: block; + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: #30bab6; + background: -webkit-linear-gradient(top, #00B4DB, #240b36); + background: -o-linear-gradient(top, #00B4DB, #240b36); + background: -moz-linear-gradient(top, #00B4DB, #240b36); + background: linear-gradient(top, #00B4DB, #240b36); + opacity: 0.8; + } + + .how-btn1 { + border-radius: 25px; + background-color: #240b36; + padding-right: 20px; + padding-left: 20px; + } + + .how-btn1:hover { + background-color: #333333; + } + + .wrappic1 { + display: block; + flex-grow: 1; + } + + .wrappic1 img { + max-width: 100%; + } + + .how-social { + color: #fff; + font-size: 22px; + + background-color: transparent; + border: 2px solid #fff; + border-radius: 2px; + } + + .how-social:hover { + background-color: #240b36; + color: #fff; + } + + .focus-in0:focus::-webkit-input-placeholder { color:transparent; } + .focus-in0:focus:-moz-placeholder { color:transparent; } + .focus-in0:focus::-moz-placeholder { color:transparent; } + .focus-in0:focus:-ms-input-placeholder { color:transparent; } + .hov-cl0:hover {color: #fff;} + .hov-bg0:hover {background-color: #fff;} + + @media (max-width: 1400px) { + .respon1 { + padding: 15px; + } + } + + @media (max-width: 1200px) { + .m-0-xl {margin: 0;} + .m-lr-0-xl {margin-left: 0; margin-right: 0;} + .m-lr-15-xl {margin-left: 15px; margin-right: 15px;} + .m-l-0-xl {margin-left: 0;} + .m-r-0-xl {margin-right: 0;} + .m-l-15-xl {margin-left: 15px;} + .m-r-15-xl {margin-right: 15px;} + + .p-0-xl {padding: 0;} + .p-lr-0-xl {padding-left: 0; padding-right: 0;} + .p-lr-15-xl {padding-left: 15px; padding-right: 15px;} + .p-l-0-xl {padding-left: 0;} + .p-r-0-xl {padding-right: 0;} + .p-l-15-xl {padding-left: 15px;} + .p-r-15-xl {padding-right: 15px;} + + .w-full-xl {width: 100%;} + + .respon1 { + flex-direction: column; + align-items: center; + } + + .respon2 { + text-align: center; + } + + .respon3 { + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; + flex-direction: column; + align-items: center; + } + } + + @media (max-width: 992px) { + .m-0-lg {margin: 0;} + .m-lr-0-lg {margin-left: 0; margin-right: 0;} + .m-lr-15-lg {margin-left: 15px; margin-right: 15px;} + .m-l-0-lg {margin-left: 0;} + .m-r-0-lg {margin-right: 0;} + .m-l-15-lg {margin-left: 15px;} + .m-r-15-lg {margin-right: 15px;} + + .p-0-lg {padding: 0;} + .p-lr-0-lg {padding-left: 0; padding-right: 0;} + .p-lr-15-lg {padding-left: 15px; padding-right: 15px;} + .p-l-0-lg {padding-left: 0;} + .p-r-0-lg{padding-right: 0;} + .p-l-15-lg {padding-left: 15px;} + .p-r-15-lg {padding-right: 15px;} + + .w-full-lg {width: 100%;} + } + + @media (max-width: 768px) { + .m-0-md {margin: 0;} + .m-lr-0-md {margin-left: 0; margin-right: 0;} + .m-lr-15-md {margin-left: 15px; margin-right: 15px;} + .m-l-0-md {margin-left: 0;} + .m-r-0-md {margin-right: 0;} + .m-l-15-md {margin-left: 15px;} + .m-r-15-md {margin-right: 15px;} + + .p-0-md {padding: 0;} + .p-lr-0-md {padding-left: 0; padding-right: 0;} + .p-lr-15-md {padding-left: 15px; padding-right: 15px;} + .p-l-0-md {padding-left: 0;} + .p-r-0-md{padding-right: 0;} + .p-l-15-md {padding-left: 15px;} + .p-r-15-md {padding-right: 15px;} + + .w-full-md {width: 100%;} + } + + @media (max-width: 576px) { + .m-0-sm {margin: 0;} + .m-lr-0-sm {margin-left: 0; margin-right: 0;} + .m-lr-15-sm {margin-left: 15px; margin-right: 15px;} + .m-l-0-sm {margin-left: 0;} + .m-r-0-sm {margin-right: 0;} + .m-l-15-sm {margin-left: 15px;} + .m-r-15-sm {margin-right: 15px;} + + .p-0-sm {padding: 0;} + .p-lr-0-sm {padding-left: 0; padding-right: 0;} + .p-lr-15-sm {padding-left: 15px; padding-right: 15px;} + .p-l-0-sm {padding-left: 0;} + .p-r-0-sm{padding-right: 0;} + .p-l-15-sm {padding-left: 15px;} + .p-r-15-sm {padding-right: 15px;} + + .w-full-sm {width: 100%;} + + .respon4 { + font-size: 50px; + } + + .respon5 { + padding-left: 20px; + padding-right: 14px; + padding-bottom: 50px; + } + + } + + @media (max-width: 480px) { + .m-0-ssm {margin: 0;} + .m-lr-0-ssm {margin-left: 0; margin-right: 0;} + .m-lr-15-ssm {margin-left: 15px; margin-right: 15px;} + .m-l-0-ssm {margin-left: 0;} + .m-r-0-ssm {margin-right: 0;} + .m-l-15-ssm {margin-left: 15px;} + .m-r-15-ssm {margin-right: 15px;} + + .p-0-ssm {padding: 0;} + .p-lr-0-ssm {padding-left: 0; padding-right: 0;} + .p-lr-15-ssm {padding-left: 15px; padding-right: 15px;} + .p-l-0-ssm {padding-left: 0;} + .p-r-0-ssm{padding-right: 0;} + .p-l-15-ssm {padding-left: 15px;} + .p-r-15-ssm {padding-right: 15px;} + + .w-full-ssm {width: 100%;} + } + + diff --git a/docs/.gitignore b/docs/.gitignore index 38353fb5..eccfbb30 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,5 @@ .vuepress/dist node_modules ts +api.md +api/ diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index f3b735b8..562ed723 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -39,7 +39,7 @@ module.exports = { // Custom text for edit link. Defaults to "Edit this page" editLinkText: "Edit this page on GitHub", // Custom navbar values - nav: [{ text: "Setup", link: "/setup/" }], + nav: [{ text: "Setup", link: "/setup/" }, { text: "API", link: "/api/index.html" }], // Custom sidebar values sidebar: [ "/", diff --git a/docs/README.md b/docs/README.md index 082bb05c..4356775b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ home: true heroImage: /logo.png actionText: Get Started → actionLink: /guide/ -footer: MIT Licensed | Copyright © 2016-present jc21.com +footer: MIT Licensed | Copyright © 2016-2021 jc21.com ---
@@ -37,3 +37,37 @@ footer: MIT Licensed | Copyright © 2016-present jc21.com

Configure other users to either view or manage their own hosts. Full access permissions are available.

+ +### Quick Setup + +1. Install Docker and Docker-Compose + +- [Docker Install documentation](https://docs.docker.com/install/) +- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) + +2. Create a docker-compose.yml file similar to this: + +```yml +version: '3' +services: + app: + image: 'jc21/nginx-proxy-manager:3' + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data +``` + +3. Bring up your stack + +```bash +docker-compose up -d +``` + +4. Log in to the Admin UI + +When your docker container is running, connect to it on port `81` for the admin interface. + +[http://127.0.0.1:81](http://127.0.0.1:81) diff --git a/docs/setup/README.md b/docs/setup/README.md index 99fbd7bb..56eb8618 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -1,22 +1,6 @@ # Full Setup Instructions -## MySQL Database - -If you opt for the MySQL configuration you will have to provide the database server yourself. You can also use MariaDB. Here are the minimum supported versions: - -- MySQL v5.7.8+ -- MariaDB v10.2.7+ - -It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples -are going to use. - -::: warning - -When using a `mariadb` database, the NPM configuration file should still use the `mysql` engine! - -::: - -## Running the App +### Running the App Via `docker-compose`: @@ -24,8 +8,8 @@ Via `docker-compose`: version: "3" services: app: - image: 'jc21/nginx-proxy-manager:latest' - restart: unless-stopped + image: 'jc21/nginx-proxy-manager:3' + restart: always ports: # Public HTTP Port: - '80:80' @@ -33,46 +17,20 @@ services: - '443:443' # Admin Web Port: - '81:81' - # Add any other Stream port you want to expose - # - '21:21' # FTP environment: - # These are the settings to access your db - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "npm" - DB_MYSQL_PASSWORD: "npm" - DB_MYSQL_NAME: "npm" - # If you would rather use Sqlite uncomment this - # and remove all DB_MYSQL_* lines above - # DB_SQLITE_FILE: "/data/database.sqlite" # Uncomment this if IPv6 is not enabled on your host # DISABLE_IPV6: 'true' volumes: - ./data:/data - - ./letsencrypt:/etc/letsencrypt - depends_on: - - db - db: - image: 'jc21/mariadb-aria:latest' - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: 'npm' - MYSQL_DATABASE: 'npm' - MYSQL_USER: 'npm' - MYSQL_PASSWORD: 'npm' - volumes: - - ./data/mysql:/var/lib/mysql ``` -_Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use Sqlite._ - Then: ```bash docker-compose up -d ``` -## Running on Raspberry PI / ARM devices +### Running on Raspberry PI / ARM devices The docker images support the following architectures: - amd64 @@ -89,121 +47,12 @@ for a list of supported architectures and if you want one that doesn't exist, Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) on Raspbian. -Via `docker-compose`: -```yml -version: "3" -services: - app: - image: 'jc21/nginx-proxy-manager:latest' - restart: unless-stopped - ports: - # Public HTTP Port: - - '80:80' - # Public HTTPS Port: - - '443:443' - # Admin Web Port: - - '81:81' - environment: - # These are the settings to access your db - DB_MYSQL_HOST: "db" - DB_MYSQL_PORT: 3306 - DB_MYSQL_USER: "changeuser" - DB_MYSQL_PASSWORD: "changepass" - DB_MYSQL_NAME: "npm" - # If you would rather use Sqlite uncomment this - # and remove all DB_MYSQL_* lines above - # DB_SQLITE_FILE: "/data/database.sqlite" - # Uncomment this if IPv6 is not enabled on your host - # DISABLE_IPV6: 'true' - volumes: - - ./data/nginx-proxy-manager:/data - - ./letsencrypt:/etc/letsencrypt - depends_on: - - db - db: - image: yobasystems/alpine-mariadb:latest - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: "changeme" - MYSQL_DATABASE: "npm" - MYSQL_USER: "changeuser" - MYSQL_PASSWORD: "changepass" - volumes: - - ./data/mariadb:/var/lib/mysql -``` - -_Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` var> - -Then: - -```bash -docker-compose up -d -``` - -## Initial Run +### Initial Run After the app is running for the first time, the following will happen: 1. The database will initialize with table structures 2. GPG keys will be generated and saved in the configuration file -3. A default admin user will be created This process can take a couple of minutes depending on your machine. - - -## Default Administrator User - -``` -Email: admin@example.com -Password: changeme -``` - -Immediately after logging in with this default user you will be asked to modify your details and change your password. - -## Configuration File - -::: warning - -This section is meant for advanced users - -::: - -If you would like more control over the database settings you can define a custom config JSON file. - - -Here's an example for `sqlite` configuration as it is generated from the environment variables: - -```json -{ - "database": { - "engine": "knex-native", - "knex": { - "client": "sqlite3", - "connection": { - "filename": "/data/database.sqlite" - }, - "useNullAsDefault": true - } - } -} -``` - -You can modify the `knex` object with your custom configuration, but note that not all knex clients might be installed in the image. - -Once you've created your configuration file you can mount it to `/app/config/production.json` inside you container using: - -``` -[...] -services: - app: - image: 'jc21/nginx-proxy-manager:latest' - [...] - volumes: - - ./config.json:/app/config/production.json - [...] -[...] -``` - -**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. -These keys affect the login and session management of the application. If these keys change for any reason, all users will be logged out. diff --git a/frontend/.babelrc b/frontend/.babelrc deleted file mode 100644 index 54071ecd..00000000 --- a/frontend/.babelrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "presets": [ - [ - "env", - { - "targets": { - "browsers": [ - "Chrome >= 65" - ] - }, - "debug": false, - "modules": false, - "useBuiltIns": "usage" - } - ] - ] -} \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 00000000..fe9267b2 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,2 @@ +PORT=9000 +IMAGE_INLINE_SIZE_LIMIT=20000 diff --git a/frontend/.eslintrc b/frontend/.eslintrc new file mode 100644 index 00000000..ca27c839 --- /dev/null +++ b/frontend/.eslintrc @@ -0,0 +1,119 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "prettier", + "import", + "react-hooks" + ], + "extends": [ + "react-app", + "eslint-config-prettier", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "jest": true, + "browser": true, + "commonjs": true + }, + "rules": { + "prettier/prettier": [ + "error" + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } + ], + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": [ + "off" + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "off" + ], + "@typescript-eslint/explicit-member-accessibility": [ + "off" + ], + "@typescript-eslint/no-empty-function": [ + "off" + ], + "@typescript-eslint/no-explicit-any": [ + "off" + ], + "@typescript-eslint/no-non-null-assertion": [ + "off" + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": [ + "camelCase", + "PascalCase", + "UPPER_CASE" + ], + "leadingUnderscore": "allow", + "trailingUnderscore": "allow" + } + ], + "react-hooks/rules-of-hooks": [ + "error" + ], + "react-hooks/exhaustive-deps": [ + "warn", + { + "additionalHooks": "useAction|useReduxAction" + } + ], + "no-restricted-globals": [ + "off" + ], + "import/extensions": 0, // We let webpack handle resolving file extensions + "import/order": [ + "error", + { + "alphabetize": { + "order": "asc", + "caseInsensitive": true + }, + "newlines-between": "always", + "pathGroups": [ + { + "pattern": "@(react)", + "group": "external", + "position": "before" + }, + { + "pattern": "@/@(fixtures|jest)/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/**", + "group": "internal" + } + ], + "pathGroupsExcludedImportTypes": [ + "builtin", + "internal" + ], + "groups": [ + "builtin", + "external", + "internal", + [ + "parent", + "sibling", + "index" + ] + ] + } + ] + } +} \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index c8f4b4f9..aa97ba4b 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,4 +1,4 @@ -dist -node_modules -webpack_stats.html -yarn-error.log +.eslintcache +coverage +junit.xml +eslint.xml diff --git a/backend/.prettierrc b/frontend/.prettierrc similarity index 70% rename from backend/.prettierrc rename to frontend/.prettierrc index fefbcfa6..00b01a49 100644 --- a/backend/.prettierrc +++ b/frontend/.prettierrc @@ -1,9 +1,9 @@ { - "printWidth": 320, - "tabWidth": 4, + "printWidth": 80, + "tabWidth": 2, "useTabs": true, "semi": true, - "singleQuote": true, + "singleQuote": false, "bracketSpacing": true, "jsxBracketSameLine": true, "trailingComma": "all", diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..2fa78e71 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,44 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/frontend/fonts/feather b/frontend/fonts/feather deleted file mode 120000 index 440203ba..00000000 --- a/frontend/fonts/feather +++ /dev/null @@ -1 +0,0 @@ -../node_modules/tabler-ui/dist/assets/fonts/feather \ No newline at end of file diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff deleted file mode 100644 index 96d8768e..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 deleted file mode 100644 index e97a2218..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff deleted file mode 100644 index 0829caef..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 deleted file mode 100644 index 7c901cd8..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff deleted file mode 100644 index 99652481..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 deleted file mode 100644 index 343e5ba8..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff deleted file mode 100644 index 92c3260e..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff and /dev/null differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 deleted file mode 100644 index d552543b..00000000 Binary files a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 and /dev/null differ diff --git a/frontend/globalSetup.js b/frontend/globalSetup.js new file mode 100644 index 00000000..7ba92707 --- /dev/null +++ b/frontend/globalSetup.js @@ -0,0 +1,4 @@ +module.exports = async () => { + process.env.TZ = "Australia/Queensland"; + process.env.IMAGE_INLINE_SIZE_LIMIT = "20000"; +}; diff --git a/frontend/html/index.ejs b/frontend/html/index.ejs deleted file mode 100644 index ae08b012..00000000 --- a/frontend/html/index.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% var title = 'Nginx Proxy Manager' %> -<%- include partials/header.ejs %> - -
- -
- - -<%- include partials/footer.ejs %> diff --git a/frontend/html/login.ejs b/frontend/html/login.ejs deleted file mode 100644 index bc4b9a27..00000000 --- a/frontend/html/login.ejs +++ /dev/null @@ -1,9 +0,0 @@ -<% var title = 'Login – Nginx Proxy Manager' %> -<%- include partials/header.ejs %> - -
- -
- - -<%- include partials/footer.ejs %> diff --git a/frontend/html/partials/footer.ejs b/frontend/html/partials/footer.ejs deleted file mode 100644 index 7fb2bd61..00000000 --- a/frontend/html/partials/footer.ejs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/frontend/html/partials/header.ejs b/frontend/html/partials/header.ejs deleted file mode 100644 index b8d88331..00000000 --- a/frontend/html/partials/header.ejs +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - <%- title %> - - - - - - - - - - - - - - diff --git a/frontend/images b/frontend/images deleted file mode 120000 index 37c31854..00000000 --- a/frontend/images +++ /dev/null @@ -1 +0,0 @@ -./node_modules/tabler-ui/dist/assets/images \ No newline at end of file diff --git a/frontend/jest.eslint.js b/frontend/jest.eslint.js new file mode 100644 index 00000000..04ee4f2c --- /dev/null +++ b/frontend/jest.eslint.js @@ -0,0 +1,6 @@ +module.exports = { + displayName: "ESLint", + runner: "jest-runner-eslint", + testMatch: ["/src/**/*.(js|jsx|ts|tsx)"], + watchPlugins: ["jest-runner-eslint/watch-fix"], +}; diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js deleted file mode 100644 index 9d11d268..00000000 --- a/frontend/js/app/api.js +++ /dev/null @@ -1,694 +0,0 @@ -const $ = require('jquery'); -const _ = require('underscore'); -const Tokens = require('./tokens'); - -/** - * @param {String} message - * @param {*} debug - * @param {Number} code - * @constructor - */ -const ApiError = function (message, debug, code) { - let temp = Error.call(this, message); - temp.name = this.name = 'ApiError'; - this.stack = temp.stack; - this.message = temp.message; - this.debug = debug; - this.code = code; -}; - -ApiError.prototype = Object.create(Error.prototype, { - constructor: { - value: ApiError, - writable: true, - configurable: true - } -}); - -/** - * - * @param {String} verb - * @param {String} path - * @param {Object} [data] - * @param {Object} [options] - * @returns {Promise} - */ -function fetch(verb, path, data, options) { - options = options || {}; - - return new Promise(function (resolve, reject) { - let api_url = '/api/'; - let url = api_url + path; - let token = Tokens.getTopToken(); - - if ((typeof options.contentType === 'undefined' || options.contentType.match(/json/im)) && typeof data === 'object') { - data = JSON.stringify(data); - } - - $.ajax({ - url: url, - data: typeof data === 'object' ? JSON.stringify(data) : data, - type: verb, - dataType: 'json', - contentType: options.contentType || 'application/json; charset=UTF-8', - processData: options.processData || true, - crossDomain: true, - timeout: options.timeout ? options.timeout : 180000, - xhrFields: { - withCredentials: true - }, - - beforeSend: function (xhr) { - xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); - }, - - success: function (data, textStatus, response) { - let total = response.getResponseHeader('X-Dataset-Total'); - if (total !== null) { - resolve({ - data: data, - pagination: { - total: parseInt(total, 10), - offset: parseInt(response.getResponseHeader('X-Dataset-Offset'), 10), - limit: parseInt(response.getResponseHeader('X-Dataset-Limit'), 10) - } - }); - } else { - resolve(response); - } - }, - - error: function (xhr, status, error_thrown) { - let code = 400; - - if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') { - error_thrown = xhr.responseJSON.error.message; - code = xhr.responseJSON.error.code || 500; - } - - reject(new ApiError(error_thrown, xhr.responseText, code)); - } - }); - }); -} - -/** - * - * @param {Array} expand - * @returns {String} - */ -function makeExpansionString(expand) { - let items = []; - _.forEach(expand, function (exp) { - items.push(encodeURIComponent(exp)); - }); - - return items.join(','); -} - -/** - * @param {String} path - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ -function getAllObjects(path, expand, query) { - let params = []; - - if (typeof expand === 'object' && expand !== null && expand.length) { - params.push('expand=' + makeExpansionString(expand)); - } - - if (typeof query === 'string') { - params.push('query=' + query); - } - - return fetch('get', path + (params.length ? '?' + params.join('&') : '')); -} - -function FileUpload(path, fd) { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - let token = Tokens.getTopToken(); - - xhr.open('POST', '/api/' + path); - xhr.overrideMimeType('text/plain'); - xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); - xhr.send(fd); - - xhr.onreadystatechange = function () { - if (this.readyState === XMLHttpRequest.DONE) { - if (xhr.status !== 200 && xhr.status !== 201) { - try { - reject(new Error('Upload failed: ' + JSON.parse(xhr.responseText).error.message)); - } catch (err) { - reject(new Error('Upload failed: ' + xhr.status)); - } - } else { - resolve(xhr.responseText); - } - } - }; - }); -} - -module.exports = { - status: function () { - return fetch('get', ''); - }, - - Tokens: { - - /** - * @param {String} identity - * @param {String} secret - * @param {Boolean} [wipe] Will wipe the stack before adding to it again if login was successful - * @returns {Promise} - */ - login: function (identity, secret, wipe) { - return fetch('post', 'tokens', {identity: identity, secret: secret}) - .then(response => { - if (response.token) { - if (wipe) { - Tokens.clearTokens(); - } - - // Set storage token - Tokens.addToken(response.token); - return response.token; - } else { - Tokens.clearTokens(); - throw(new Error('No token returned')); - } - }); - }, - - /** - * @returns {Promise} - */ - refresh: function () { - return fetch('get', 'tokens') - .then(response => { - if (response.token) { - Tokens.setCurrentToken(response.token); - return response.token; - } else { - Tokens.clearTokens(); - throw(new Error('No token returned')); - } - }); - } - }, - - Users: { - - /** - * @param {Number|String} user_id - * @param {Array} [expand] - * @returns {Promise} - */ - getById: function (user_id, expand) { - return fetch('get', 'users/' + user_id + (typeof expand === 'object' && expand.length ? '?expand=' + makeExpansionString(expand) : '')); - }, - - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('users', expand, query); - }, - - /** - * @param {Object} data - * @returns {Promise} - */ - create: function (data) { - return fetch('post', 'users', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'users/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'users/' + id); - }, - - /** - * - * @param {Number} id - * @param {Object} auth - * @returns {Promise} - */ - setPassword: function (id, auth) { - return fetch('put', 'users/' + id + '/auth', auth); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - loginAs: function (id) { - return fetch('post', 'users/' + id + '/login'); - }, - - /** - * - * @param {Number} id - * @param {Object} perms - * @returns {Promise} - */ - setPermissions: function (id, perms) { - return fetch('put', 'users/' + id + '/permissions', perms); - } - }, - - Nginx: { - - ProxyHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/proxy-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/proxy-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/proxy-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/proxy-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/proxy-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/proxy-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/proxy-hosts/' + id + '/disable'); - } - }, - - RedirectionHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/redirection-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/redirection-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/redirection-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/redirection-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/redirection-hosts/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - setCerts: function (id, form_data) { - return FileUpload('nginx/redirection-hosts/' + id + '/certificates', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/redirection-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/redirection-hosts/' + id + '/disable'); - } - }, - - Streams: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/streams', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/streams', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/streams/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/streams/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/streams/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/streams/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/streams/' + id + '/disable'); - } - }, - - DeadHosts: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/dead-hosts', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/dead-hosts', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/dead-hosts/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/dead-hosts/' + id); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - get: function (id) { - return fetch('get', 'nginx/dead-hosts/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - setCerts: function (id, form_data) { - return FileUpload('nginx/dead-hosts/' + id + '/certificates', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - enable: function (id) { - return fetch('post', 'nginx/dead-hosts/' + id + '/enable'); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - disable: function (id) { - return fetch('post', 'nginx/dead-hosts/' + id + '/disable'); - } - }, - - AccessLists: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/access-lists', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - return fetch('post', 'nginx/access-lists', data); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/access-lists/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/access-lists/' + id); - } - }, - - Certificates: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('nginx/certificates', expand, query); - }, - - /** - * @param {Object} data - */ - create: function (data) { - - const timeout = 180000 + (data && data.meta && data.meta.propagation_seconds ? Number(data.meta.propagation_seconds) * 1000 : 0); - return fetch('post', 'nginx/certificates', data, {timeout}); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'nginx/certificates/' + id, data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - delete: function (id) { - return fetch('delete', 'nginx/certificates/' + id); - }, - - /** - * @param {Number} id - * @param {FormData} form_data - * @params {Promise} - */ - upload: function (id, form_data) { - return FileUpload('nginx/certificates/' + id + '/upload', form_data); - }, - - /** - * @param {FormData} form_data - * @params {Promise} - */ - validate: function (form_data) { - return FileUpload('nginx/certificates/validate', form_data); - }, - - /** - * @param {Number} id - * @returns {Promise} - */ - renew: function (id, timeout = 180000) { - return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout}); - } - } - }, - - AuditLog: { - /** - * @param {Array} [expand] - * @param {String} [query] - * @returns {Promise} - */ - getAll: function (expand, query) { - return getAllObjects('audit-log', expand, query); - } - }, - - Reports: { - - /** - * @returns {Promise} - */ - getHostStats: function () { - return fetch('get', 'reports/hosts'); - } - }, - - Settings: { - - /** - * @param {String} setting_id - * @returns {Promise} - */ - getById: function (setting_id) { - return fetch('get', 'settings/' + setting_id); - }, - - /** - * @returns {Promise} - */ - getAll: function () { - return getAllObjects('settings'); - }, - - /** - * @param {Object} data - * @param {Number} data.id - * @returns {Promise} - */ - update: function (data) { - let id = data.id; - delete data.id; - return fetch('put', 'settings/' + id, data); - } - } -}; diff --git a/frontend/js/app/audit-log/list/item.ejs b/frontend/js/app/audit-log/list/item.ejs deleted file mode 100644 index 84743c8d..00000000 --- a/frontend/js/app/audit-log/list/item.ejs +++ /dev/null @@ -1,80 +0,0 @@ - -
- -
- - -
- <% if (user.is_deleted) { - %> - <%- user.name %> - <% - } else { - %> - <%- user.name %> - <% - } - %> -
- - -
- <% - var items = []; - switch (object_type) { - case 'proxy-host': - %> <% - items = meta.domain_names; - break; - case 'redirection-host': - %> <% - items = meta.domain_names; - break; - case 'stream': - %> <% - items.push(meta.incoming_port); - break; - case 'dead-host': - %> <% - items = meta.domain_names; - break; - case 'access-list': - %> <% - items.push(meta.name); - break; - case 'user': - %> <% - items.push(meta.name); - break; - case 'certificate': - %> <% - if (meta.provider === 'letsencrypt') { - items = meta.domain_names; - } else { - items.push(meta.nice_name); - } - break; - } - %> <%- i18n('audit-log', action, {name: i18n('audit-log', object_type)}) %> - — - <% - if (items && items.length) { - items.map(function(item) { - %> - <%- item %> - <% - }); - } else { - %> - #<%- object_id %> - <% - } - %> -
-
- <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %> -
- - - <%- i18n('audit-log', 'view-meta') %> - diff --git a/frontend/js/app/audit-log/list/item.js b/frontend/js/app/audit-log/list/item.js deleted file mode 100644 index 862ffc22..00000000 --- a/frontend/js/app/audit-log/list/item.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const Controller = require('../../controller'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - meta: 'a.meta' - }, - - events: { - 'click @ui.meta': function (e) { - e.preventDefault(); - Controller.showAuditMeta(this.model); - } - }, - - templateContext: { - more: function() { - switch (this.object_type) { - case 'redirection-host': - case 'stream': - case 'proxy-host': - return this.meta.domain_names.join(', '); - } - - return '#' + (this.object_id || '?'); - } - } -}); diff --git a/frontend/js/app/audit-log/list/main.ejs b/frontend/js/app/audit-log/list/main.ejs deleted file mode 100644 index ec3cf2a2..00000000 --- a/frontend/js/app/audit-log/list/main.ejs +++ /dev/null @@ -1,9 +0,0 @@ - -   - User - Event -   - - - - diff --git a/frontend/js/app/audit-log/list/main.js b/frontend/js/app/audit-log/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/audit-log/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/audit-log/main.ejs b/frontend/js/app/audit-log/main.ejs deleted file mode 100644 index acaa8b49..00000000 --- a/frontend/js/app/audit-log/main.ejs +++ /dev/null @@ -1,15 +0,0 @@ -
-
-
-

<%- i18n('audit-log', 'title') %>

-
-
-
-
-
- -
-
- -
-
diff --git a/frontend/js/app/audit-log/main.js b/frontend/js/app/audit-log/main.js deleted file mode 100644 index ec9b5368..00000000 --- a/frontend/js/app/audit-log/main.js +++ /dev/null @@ -1,53 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const AuditLogModel = require('../../models/audit-log'); -const ListView = require('./list/main'); -const template = require('./main.ejs'); -const ErrorView = require('../error/main'); -const EmptyView = require('../empty/main'); - -module.exports = Mn.View.extend({ - id: 'audit-log', - template: template, - - ui: { - list_region: '.list-region', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - onRender: function () { - let view = this; - - App.Api.AuditLog.getAll(['user']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new AuditLogModel.Collection(response) - })); - } else { - view.showChildView('list_region', new EmptyView({ - title: App.i18n('audit-log', 'empty'), - subtitle: App.i18n('audit-log', 'empty-subtitle') - })); - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showAuditLog(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/audit-log/meta.ejs b/frontend/js/app/audit-log/meta.ejs deleted file mode 100644 index 98a2d973..00000000 --- a/frontend/js/app/audit-log/meta.ejs +++ /dev/null @@ -1,27 +0,0 @@ - diff --git a/frontend/js/app/audit-log/meta.js b/frontend/js/app/audit-log/meta.js deleted file mode 100644 index 815cdfac..00000000 --- a/frontend/js/app/audit-log/meta.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./meta.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog wide' -}); diff --git a/frontend/js/app/cache.js b/frontend/js/app/cache.js deleted file mode 100644 index 6d1fbc4f..00000000 --- a/frontend/js/app/cache.js +++ /dev/null @@ -1,10 +0,0 @@ -const UserModel = require('../models/user'); - -let cache = { - User: new UserModel.Model(), - locale: 'en', - version: null -}; - -module.exports = cache; - diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js deleted file mode 100644 index 902659be..00000000 --- a/frontend/js/app/controller.js +++ /dev/null @@ -1,434 +0,0 @@ -const Backbone = require('backbone'); -const Cache = require('./cache'); -const Tokens = require('./tokens'); - -module.exports = { - - /** - * @param {String} route - * @param {Object} [options] - * @returns {Boolean} - */ - navigate: function (route, options) { - options = options || {}; - Backbone.history.navigate(route.toString(), options); - return true; - }, - - /** - * Login - */ - showLogin: function () { - window.location = '/login'; - }, - - /** - * Users - */ - showUsers: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './users/main'], (App, View) => { - controller.navigate('/users'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * User Form - * - * @param [model] - */ - showUserForm: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './user/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Permissions Form - * - * @param model - */ - showUserPermissions: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './user/permissions'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Password Form - * - * @param model - */ - showUserPasswordForm: function (model) { - if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) { - require(['./main', './user/password'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * User Delete Confirm - * - * @param model - */ - showUserDeleteConfirm: function (model) { - if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { - require(['./main', './user/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Dashboard - */ - showDashboard: function () { - let controller = this; - - require(['./main', './dashboard/main'], (App, View) => { - controller.navigate('/'); - App.UI.showAppContent(new View()); - }); - }, - - /** - * Nginx Proxy Hosts - */ - showNginxProxy: function () { - if (Cache.User.isAdmin() || Cache.User.canView('proxy_hosts')) { - let controller = this; - - require(['./main', './nginx/proxy/main'], (App, View) => { - controller.navigate('/nginx/proxy'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Proxy Host Form - * - * @param [model] - */ - showNginxProxyForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { - require(['./main', './nginx/proxy/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Proxy Host Delete Confirm - * - * @param model - */ - showNginxProxyDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('proxy_hosts')) { - require(['./main', './nginx/proxy/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Redirection Hosts - */ - showNginxRedirection: function () { - if (Cache.User.isAdmin() || Cache.User.canView('redirection_hosts')) { - let controller = this; - - require(['./main', './nginx/redirection/main'], (App, View) => { - controller.navigate('/nginx/redirection'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Redirection Host Form - * - * @param [model] - */ - showNginxRedirectionForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { - require(['./main', './nginx/redirection/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Proxy Redirection Delete Confirm - * - * @param model - */ - showNginxRedirectionDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('redirection_hosts')) { - require(['./main', './nginx/redirection/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Stream Hosts - */ - showNginxStream: function () { - if (Cache.User.isAdmin() || Cache.User.canView('streams')) { - let controller = this; - - require(['./main', './nginx/stream/main'], (App, View) => { - controller.navigate('/nginx/stream'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Stream Form - * - * @param [model] - */ - showNginxStreamForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { - require(['./main', './nginx/stream/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Stream Delete Confirm - * - * @param model - */ - showNginxStreamDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('streams')) { - require(['./main', './nginx/stream/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Dead Hosts - */ - showNginxDead: function () { - if (Cache.User.isAdmin() || Cache.User.canView('dead_hosts')) { - let controller = this; - - require(['./main', './nginx/dead/main'], (App, View) => { - controller.navigate('/nginx/404'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Dead Host Form - * - * @param [model] - */ - showNginxDeadForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { - require(['./main', './nginx/dead/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Dead Host Delete Confirm - * - * @param model - */ - showNginxDeadDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('dead_hosts')) { - require(['./main', './nginx/dead/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Help Dialog - * - * @param {String} title - * @param {String} content - */ - showHelp: function (title, content) { - require(['./main', './help/main'], function (App, View) { - App.UI.showModalDialog(new View({title: title, content: content})); - }); - }, - - /** - * Nginx Access - */ - showNginxAccess: function () { - if (Cache.User.isAdmin() || Cache.User.canView('access_lists')) { - let controller = this; - - require(['./main', './nginx/access/main'], (App, View) => { - controller.navigate('/nginx/access'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Access List Form - * - * @param [model] - */ - showNginxAccessListForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { - require(['./main', './nginx/access/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Access List Delete Confirm - * - * @param model - */ - showNginxAccessListDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('access_lists')) { - require(['./main', './nginx/access/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Nginx Certificates - */ - showNginxCertificates: function () { - if (Cache.User.isAdmin() || Cache.User.canView('certificates')) { - let controller = this; - - require(['./main', './nginx/certificates/main'], (App, View) => { - controller.navigate('/nginx/certificates'); - App.UI.showAppContent(new View()); - }); - } - }, - - /** - * Nginx Certificate Form - * - * @param [model] - */ - showNginxCertificateForm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/form'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Certificate Renew - * - * @param model - */ - showNginxCertificateRenew: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/renew'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Certificate Delete Confirm - * - * @param model - */ - showNginxCertificateDeleteConfirm: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/delete'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Audit Log - */ - showAuditLog: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './audit-log/main'], (App, View) => { - controller.navigate('/audit-log'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * Audit Log Metadata - * - * @param model - */ - showAuditMeta: function (model) { - if (Cache.User.isAdmin()) { - require(['./main', './audit-log/meta'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - }, - - /** - * Settings - */ - showSettings: function () { - let controller = this; - if (Cache.User.isAdmin()) { - require(['./main', './settings/main'], (App, View) => { - controller.navigate('/settings'); - App.UI.showAppContent(new View()); - }); - } else { - this.showDashboard(); - } - }, - - /** - * Settings Item Form - * - * @param model - */ - showSettingForm: function (model) { - if (Cache.User.isAdmin()) { - if (model.get('id') === 'default-site') { - require(['./main', './settings/default-site/main'], function (App, View) { - App.UI.showModalDialog(new View({model: model})); - }); - } - } - }, - - /** - * Logout - */ - logout: function () { - Tokens.dropTopToken(); - this.showLogin(); - } -}; diff --git a/frontend/js/app/dashboard/main.ejs b/frontend/js/app/dashboard/main.ejs deleted file mode 100644 index c00aa6d0..00000000 --- a/frontend/js/app/dashboard/main.ejs +++ /dev/null @@ -1,67 +0,0 @@ - - -<% if (columns) { %> -
- <% if (canShow('proxy_hosts')) { %> - - <% } %> - - <% if (canShow('redirection_hosts')) { %> - - <% } %> - - <% if (canShow('streams')) { %> - - <% } %> - - <% if (canShow('dead_hosts')) { %> - - <% } %> -
-<% } %> diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js deleted file mode 100644 index c2e82f85..00000000 --- a/frontend/js/app/dashboard/main.js +++ /dev/null @@ -1,92 +0,0 @@ -const Mn = require('backbone.marionette'); -const Cache = require('../cache'); -const Controller = require('../controller'); -const Api = require('../api'); -const Helpers = require('../../lib/helpers'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - id: 'dashboard', - columns: 0, - - stats: {}, - - ui: { - links: 'a' - }, - - events: { - 'click @ui.links': function (e) { - e.preventDefault(); - Controller.navigate($(e.currentTarget).attr('href'), true); - } - }, - - templateContext: function () { - let view = this; - - return { - getUserName: function () { - return Cache.User.get('nickname') || Cache.User.get('name'); - }, - - getHostStat: function (type) { - if (view.stats && typeof view.stats.hosts !== 'undefined' && typeof view.stats.hosts[type] !== 'undefined') { - return Helpers.niceNumber(view.stats.hosts[type]); - } - - return '-'; - }, - - canShow: function (perm) { - return Cache.User.isAdmin() || Cache.User.canView(perm); - }, - - columns: view.columns - }; - }, - - onRender: function () { - let view = this; - - if (typeof view.stats.hosts === 'undefined') { - Api.Reports.getHostStats() - .then(response => { - if (!view.isDestroyed()) { - view.stats.hosts = response; - view.render(); - } - }) - .catch(err => { - console.log(err); - }); - } - }, - - /** - * @param {Object} [model] - */ - preRender: function (model) { - this.columns = 0; - - // calculate the available columns based on permissions for the objects - // and store as a variable - //let view = this; - let perms = ['proxy_hosts', 'redirection_hosts', 'streams', 'dead_hosts']; - - perms.map(perm => { - this.columns += Cache.User.isAdmin() || Cache.User.canView(perm) ? 1 : 0; - }); - - // Prevent double rendering on initial calls - if (typeof model !== 'undefined') { - this.render(); - } - }, - - initialize: function () { - this.preRender(); - this.listenTo(Cache.User, 'change', this.preRender); - } -}); diff --git a/frontend/js/app/empty/main.ejs b/frontend/js/app/empty/main.ejs deleted file mode 100644 index 11633dfc..00000000 --- a/frontend/js/app/empty/main.ejs +++ /dev/null @@ -1,11 +0,0 @@ -<% if (title) { %> -

<%- title %>

-<% } - -if (subtitle) { %> -

<%- subtitle %>

-<% } - -if (link) { %> - <%- link %> -<% } %> diff --git a/frontend/js/app/empty/main.js b/frontend/js/app/empty/main.js deleted file mode 100644 index 74998d65..00000000 --- a/frontend/js/app/empty/main.js +++ /dev/null @@ -1,33 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - className: 'text-center m-7', - template: template, - - options: { - btn_color: 'teal' - }, - - ui: { - action: 'a' - }, - - events: { - 'click @ui.action': function (e) { - e.preventDefault(); - this.getOption('action')(); - } - }, - - templateContext: function () { - return { - title: this.getOption('title'), - subtitle: this.getOption('subtitle'), - link: this.getOption('link'), - action: typeof this.getOption('action') === 'function', - btn_color: this.getOption('btn_color') - }; - } - -}); diff --git a/frontend/js/app/error/main.ejs b/frontend/js/app/error/main.ejs deleted file mode 100644 index f7fd709b..00000000 --- a/frontend/js/app/error/main.ejs +++ /dev/null @@ -1,7 +0,0 @@ - -<%= code ? '' + code + ' — ' : '' %> -<%- message %> - -<% if (retry) { %> -

<%- i18n('str', 'try-again') %> -<% } %> diff --git a/frontend/js/app/error/main.js b/frontend/js/app/error/main.js deleted file mode 100644 index 6fa85fc8..00000000 --- a/frontend/js/app/error/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'alert alert-icon alert-warning m-5', - - ui: { - retry: 'a.retry' - }, - - events: { - 'click @ui.retry': function (e) { - e.preventDefault(); - this.getOption('retry')(); - } - }, - - templateContext: function () { - return { - message: this.getOption('message'), - code: this.getOption('code'), - retry: typeof this.getOption('retry') === 'function' - }; - } - -}); diff --git a/frontend/js/app/help/main.ejs b/frontend/js/app/help/main.ejs deleted file mode 100644 index 6fb79e66..00000000 --- a/frontend/js/app/help/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/frontend/js/app/help/main.js b/frontend/js/app/help/main.js deleted file mode 100644 index b0f54374..00000000 --- a/frontend/js/app/help/main.js +++ /dev/null @@ -1,16 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog wide', - - templateContext: function () { - let content = this.getOption('content').split("\n"); - - return { - title: this.getOption('title'), - content: '

' + content.join('

') + '

' - }; - } -}); diff --git a/frontend/js/app/i18n.js b/frontend/js/app/i18n.js deleted file mode 100644 index c63cdc07..00000000 --- a/frontend/js/app/i18n.js +++ /dev/null @@ -1,23 +0,0 @@ -const Cache = ('./cache'); -const messages = require('../i18n/messages.json'); - -/** - * @param {String} namespace - * @param {String} key - * @param {Object} [data] - */ -module.exports = function (namespace, key, data) { - let locale = Cache.locale; - // check that the locale exists - if (typeof messages[locale] === 'undefined') { - locale = 'en'; - } - - if (typeof messages[locale][namespace] !== 'undefined' && typeof messages[locale][namespace][key] !== 'undefined') { - return messages[locale][namespace][key](data); - } else if (locale !== 'en' && typeof messages['en'][namespace] !== 'undefined' && typeof messages['en'][namespace][key] !== 'undefined') { - return messages['en'][namespace][key](data); - } - - return '(MISSING: ' + namespace + '/' + key + ')'; -}; diff --git a/frontend/js/app/main.js b/frontend/js/app/main.js deleted file mode 100644 index e85b4f62..00000000 --- a/frontend/js/app/main.js +++ /dev/null @@ -1,155 +0,0 @@ -const _ = require('underscore'); -const Backbone = require('backbone'); -const Mn = require('../lib/marionette'); -const Cache = require('./cache'); -const Controller = require('./controller'); -const Router = require('./router'); -const Api = require('./api'); -const Tokens = require('./tokens'); -const UI = require('./ui/main'); -const i18n = require('./i18n'); - -const App = Mn.Application.extend({ - - Cache: Cache, - Api: Api, - UI: null, - i18n: i18n, - Controller: Controller, - - region: { - el: '#app', - replaceElement: true - }, - - onStart: function (app, options) { - console.log(i18n('main', 'welcome')); - - // Check if token is coming through - if (this.getParam('token')) { - Tokens.addToken(this.getParam('token')); - } - - // Check if we are still logged in by refreshing the token - Api.status() - .then(result => { - Cache.version = [result.version.major, result.version.minor, result.version.revision].join('.'); - }) - .then(Api.Tokens.refresh) - .then(this.bootstrap) - .then(() => { - console.info(i18n('main', 'logged-in', Cache.User.attributes)); - this.bootstrapTimer(); - this.refreshTokenTimer(); - - this.UI = new UI(); - this.UI.on('render', () => { - new Router(options); - Backbone.history.start({pushState: true}); - - // Ask the admin use to change their details - if (Cache.User.get('email') === 'admin@example.com') { - Controller.showUserForm(Cache.User); - } - }); - - this.getRegion().show(this.UI); - }) - .catch(err => { - console.warn('Not logged in:', err.message); - Controller.showLogin(); - }); - }, - - History: { - replace: function (data) { - window.history.replaceState(_.extend(window.history.state || {}, data), document.title); - }, - - get: function (attr) { - return window.history.state ? window.history.state[attr] : undefined; - } - }, - - getParam: function (name) { - name = name.replace(/[\[\]]/g, '\\$&'); - let url = window.location.href; - let regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); - let results = regex.exec(url); - - if (!results) { - return null; - } - - if (!results[2]) { - return ''; - } - - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }, - - /** - * Get user and other base info to start prime the cache and the application - * - * @returns {Promise} - */ - bootstrap: function () { - return Api.Users.getById('me', ['permissions']) - .then(response => { - Cache.User.set(response); - Tokens.setCurrentName(response.nickname || response.name); - }); - }, - - /** - * Bootstraps the user from time to time - */ - bootstrapTimer: function () { - setTimeout(() => { - Api.status() - .then(result => { - let version = [result.version.major, result.version.minor, result.version.revision].join('.'); - if (version !== Cache.version) { - document.location.reload(); - } - }) - .then(this.bootstrap) - .then(() => { - this.bootstrapTimer(); - }) - .catch(err => { - if (err.message !== 'timeout' && err.code && err.code !== 400) { - console.log(err); - console.error(err.message); - console.info('Not logged in?'); - Controller.showLogin(); - } else { - this.bootstrapTimer(); - } - }); - }, 30 * 1000); // 30 seconds - }, - - refreshTokenTimer: function () { - setTimeout(() => { - return Api.Tokens.refresh() - .then(this.bootstrap) - .then(() => { - this.refreshTokenTimer(); - }) - .catch(err => { - if (err.message !== 'timeout' && err.code && err.code !== 400) { - console.log(err); - console.error(err.message); - console.info('Not logged in?'); - Controller.showLogin(); - } else { - this.refreshTokenTimer(); - } - }); - }, 10 * 60 * 1000); - } -}); - -const app = new App(); -module.exports = app; diff --git a/frontend/js/app/nginx/access/delete.ejs b/frontend/js/app/nginx/access/delete.ejs deleted file mode 100644 index 3833549a..00000000 --- a/frontend/js/app/nginx/access/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/access/delete.js b/frontend/js/app/nginx/access/delete.js deleted file mode 100644 index 4af91ab1..00000000 --- a/frontend/js/app/nginx/access/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.AccessLists.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxAccess(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/access/form.ejs b/frontend/js/app/nginx/access/form.ejs deleted file mode 100644 index 79220b14..00000000 --- a/frontend/js/app/nginx/access/form.ejs +++ /dev/null @@ -1,108 +0,0 @@ - diff --git a/frontend/js/app/nginx/access/form.js b/frontend/js/app/nginx/access/form.js deleted file mode 100644 index bb075548..00000000 --- a/frontend/js/app/nginx/access/form.js +++ /dev/null @@ -1,153 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const AccessListModel = require('../../../models/access-list'); -const template = require('./form.ejs'); -const ItemView = require('./form/item'); -const ClientView = require('./form/client'); - -require('jquery-serializejson'); - -const ItemsView = Mn.CollectionView.extend({ - childView: ItemView -}); - -const ClientsView = Mn.CollectionView.extend({ - childView: ClientView -}); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - items_region: '.items', - clients_region: '.clients', - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - access_add: 'button.access_add', - auth_add: 'button.auth_add' - }, - - regions: { - items_region: '@ui.items_region', - clients_region: '@ui.clients_region' - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let form_data = this.ui.form.serializeJSON(); - let items_data = []; - let clients_data = []; - - form_data.username.map(function (val, idx) { - if (val.trim().length) { - items_data.push({ - username: val.trim(), - password: form_data.password[idx] - }); - } - }); - - form_data.address.map(function (val, idx) { - if (val.trim().length) { - clients_data.push({ - address: val.trim(), - directive: form_data.directive[idx] - }) - } - }); - - if (!items_data.length && !clients_data.length) { - alert('You must specify at least 1 Authorization or Access rule'); - return; - } - - let data = { - name: form_data.name, - satisfy_any: !!form_data.satisfy_any, - pass_auth: !!form_data.pass_auth, - items: items_data, - clients: clients_data - }; - - console.log(data); - - let method = App.Api.Nginx.AccessLists.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.AccessLists.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxAccess(); - } - }); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - }, - 'click @ui.access_add': function (e) { - e.preventDefault(); - - let clients = this.model.get('clients'); - clients.push({}); - this.showChildView('clients_region', new ClientsView({ - collection: new Backbone.Collection(clients) - })); - }, - 'click @ui.auth_add': function (e) { - e.preventDefault(); - - let items = this.model.get('items'); - items.push({}); - this.showChildView('items_region', new ItemsView({ - collection: new Backbone.Collection(items) - })); - } - }, - - onRender: function () { - let items = this.model.get('items'); - let clients = this.model.get('clients'); - - // Ensure at least one field is shown initally - if (!items.length) items.push({}); - if (!clients.length) clients.push({}); - - this.showChildView('items_region', new ItemsView({ - collection: new Backbone.Collection(items) - })); - - this.showChildView('clients_region', new ClientsView({ - collection: new Backbone.Collection(clients) - })); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new AccessListModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/access/form/client.ejs b/frontend/js/app/nginx/access/form/client.ejs deleted file mode 100644 index 6b767b83..00000000 --- a/frontend/js/app/nginx/access/form/client.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
-
- -
-
-
-
- -
-
diff --git a/frontend/js/app/nginx/access/form/client.js b/frontend/js/app/nginx/access/form/client.js deleted file mode 100644 index b4c00e2e..00000000 --- a/frontend/js/app/nginx/access/form/client.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./client.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'row' -}); diff --git a/frontend/js/app/nginx/access/form/item.ejs b/frontend/js/app/nginx/access/form/item.ejs deleted file mode 100644 index c2435ecb..00000000 --- a/frontend/js/app/nginx/access/form/item.ejs +++ /dev/null @@ -1,10 +0,0 @@ -
-
- -
-
-
-
- -
-
diff --git a/frontend/js/app/nginx/access/form/item.js b/frontend/js/app/nginx/access/form/item.js deleted file mode 100644 index f15238dc..00000000 --- a/frontend/js/app/nginx/access/form/item.js +++ /dev/null @@ -1,7 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'row' -}); diff --git a/frontend/js/app/nginx/access/list/item.ejs b/frontend/js/app/nginx/access/list/item.ejs deleted file mode 100644 index 2ee37a50..00000000 --- a/frontend/js/app/nginx/access/list/item.ejs +++ /dev/null @@ -1,42 +0,0 @@ - -
- -
- - -
- <%- name %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - - <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %> - - - <%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %> - - - <% if (satisfy_any) { %> - <%- i18n('str', 'any') %> - <%} else { %> - <%- i18n('str', 'all') %> - <% } %> - - - <%- i18n('access-lists', 'proxy-host-count', {count: proxy_host_count}) %> - -<% if (canManage) { %> - - - -<% } %> diff --git a/frontend/js/app/nginx/access/list/item.js b/frontend/js/app/nginx/access/list/item.js deleted file mode 100644 index 4f68aead..00000000 --- a/frontend/js/app/nginx/access/list/item.js +++ /dev/null @@ -1,33 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit', - delete: 'a.delete' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListDeleteConfirm(this.model); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('access_lists') - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/access/list/main.ejs b/frontend/js/app/nginx/access/list/main.ejs deleted file mode 100644 index 7988e0c2..00000000 --- a/frontend/js/app/nginx/access/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('access-lists', 'authorization') %> - <%- i18n('access-lists', 'access') %> - <%- i18n('access-lists', 'satisfy') %> - <%- i18n('proxy-hosts', 'title') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/access/list/main.js b/frontend/js/app/nginx/access/list/main.js deleted file mode 100644 index 577a77ef..00000000 --- a/frontend/js/app/nginx/access/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('access_lists') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/access/main.ejs b/frontend/js/app/nginx/access/main.ejs deleted file mode 100644 index c245ff4a..00000000 --- a/frontend/js/app/nginx/access/main.ejs +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-

<%- i18n('access-lists', 'title') %>

-
- - <% if (showAddButton) { %> - <%- i18n('access-lists', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/access/main.js b/frontend/js/app/nginx/access/main.js deleted file mode 100644 index d14a9eb4..00000000 --- a/frontend/js/app/nginx/access/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const AccessListModel = require('../../../models/access-list'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-access', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxAccessListForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('access-lists', 'help-title'), App.i18n('access-lists', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('access_lists') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.AccessLists.getAll(['owner', 'items', 'clients']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new AccessListModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('access_lists'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('access-lists', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('access-lists', 'add') : null, - btn_color: 'teal', - permission: 'access_lists', - action: function () { - App.Controller.showNginxAccessListForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxAccess(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/certificates-list-item.ejs b/frontend/js/app/nginx/certificates-list-item.ejs deleted file mode 100644 index aa4b53ad..00000000 --- a/frontend/js/app/nginx/certificates-list-item.ejs +++ /dev/null @@ -1,18 +0,0 @@ -
- <% if (id === 'new') { %> -
- <%- i18n('all-hosts', 'new-cert') %> -
- <%- i18n('all-hosts', 'with-le') %> - <% } else if (id > 0) { %> -
- <%- provider === 'other' ? nice_name : domain_names.join(', ') %> -
- <%- i18n('ssl', provider) %> – Expires: <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> - <% } else { %> -
- <%- i18n('all-hosts', 'none') %> -
- <%- i18n('all-hosts', 'no-ssl') %> - <% } %> -
diff --git a/frontend/js/app/nginx/certificates/delete.ejs b/frontend/js/app/nginx/certificates/delete.ejs deleted file mode 100644 index b4e06866..00000000 --- a/frontend/js/app/nginx/certificates/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/delete.js b/frontend/js/app/nginx/certificates/delete.js deleted file mode 100644 index 89a2e5e8..00000000 --- a/frontend/js/app/nginx/certificates/delete.js +++ /dev/null @@ -1,34 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.save.addClass('btn-loading'); - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Nginx.Certificates.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxCertificates(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs deleted file mode 100644 index c8b1369f..00000000 --- a/frontend/js/app/nginx/certificates/form.ejs +++ /dev/null @@ -1,177 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js deleted file mode 100644 index d5d3acd3..00000000 --- a/frontend/js/app/nginx/certificates/form.js +++ /dev/null @@ -1,267 +0,0 @@ -const _ = require('underscore'); -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const CertificateModel = require('../../../models/certificate'); -const template = require('./form.ejs'); -const i18n = require('../../i18n'); -const dns_providers = sortProvidersAlphabetically(require('../../../../../global/certbot-dns-plugins')); - -require('jquery-serializejson'); -require('selectize'); - -function sortProvidersAlphabetically(obj) { - return Object.entries(obj) - .sort((a,b) => a[1].display_name.toLowerCase() > b[1].display_name.toLowerCase()) - .reduce((result, entry) => { - result[entry[0]] = entry[1]; - return result; - }, {}); -} - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - max_file_size: 102400, - - ui: { - form: 'form', - loader_content: '.loader-content', - non_loader_content: '.non-loader-content', - le_error_info: '#le-error-info', - domain_names: 'input[name="domain_names"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - other_certificate: '#other_certificate', - other_certificate_label: '#other_certificate_label', - other_certificate_key: '#other_certificate_key', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - other_certificate_key_label: '#other_certificate_key_label', - other_intermediate_certificate: '#other_intermediate_certificate', - other_intermediate_certificate_label: '#other_intermediate_certificate_label' - }, - - events: { - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - $(this).removeClass('btn-loading'); - return; - } - - let data = this.ui.form.serializeJSON(); - data.provider = this.model.get('provider'); - let ssl_files = []; - - if (data.provider === 'letsencrypt') { - if (typeof data.meta === 'undefined') data.meta = {}; - - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.split(',').map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - - // Manipulate - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - } else if (data.provider === 'other' && !this.model.hasSslFiles()) { - // check files are attached - if (!this.ui.other_certificate[0].files.length || !this.ui.other_certificate[0].files[0].size) { - alert('Certificate file is not attached'); - return; - } else { - if (this.ui.other_certificate[0].files[0].size > this.max_file_size) { - alert('Certificate file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'certificate', file: this.ui.other_certificate[0].files[0]}); - } - - if (!this.ui.other_certificate_key[0].files.length || !this.ui.other_certificate_key[0].files[0].size) { - alert('Certificate key file is not attached'); - return; - } else { - if (this.ui.other_certificate_key[0].files[0].size > this.max_file_size) { - alert('Certificate key file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'certificate_key', file: this.ui.other_certificate_key[0].files[0]}); - } - - if (this.ui.other_intermediate_certificate[0].files.length && this.ui.other_intermediate_certificate[0].files[0].size) { - if (this.ui.other_intermediate_certificate[0].files[0].size > this.max_file_size) { - alert('Intermediate Certificate file is too large (> 100kb)'); - return; - } - ssl_files.push({name: 'intermediate_certificate', file: this.ui.other_intermediate_certificate[0].files[0]}); - } - } - - this.ui.loader_content.show(); - this.ui.non_loader_content.hide(); - - // compile file data - let form_data = new FormData(); - if (data.provider === 'other' && ssl_files.length) { - ssl_files.map(function (file) { - form_data.append(file.name, file.file); - }); - } - - new Promise(resolve => { - if (data.provider === 'other') { - resolve(App.Api.Nginx.Certificates.validate(form_data)); - } else { - resolve(); - } - }) - .then(() => { - return App.Api.Nginx.Certificates.create(data); - }) - .then(result => { - this.model.set(result); - - // Now upload the certs if we need to - if (data.provider === 'other') { - return App.Api.Nginx.Certificates.upload(this.model.get('id'), form_data) - .then(result => { - this.model.set('meta', _.assign({}, this.model.get('meta'), result)); - }); - } - }) - .then(() => { - App.UI.closeModal(function () { - App.Controller.showNginxCertificates(); - }); - }) - .catch(err => { - let more_info = ''; - if (err.code === 500 && err.debug) { - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.loader_content.hide(); - this.ui.non_loader_content.show(); - }); - }, - 'change @ui.other_certificate_key': function(e){ - this.setFileName("other_certificate_key_label", e) - }, - 'change @ui.other_certificate': function(e){ - this.setFileName("other_certificate_label", e) - }, - 'change @ui.other_intermediate_certificate': function(e){ - this.setFileName("other_intermediate_certificate_label", e) - } - }, - setFileName(ui, e){ - this.getUI(ui).text(e.target.files[0].name) - }, - templateContext: { - getLetsencryptEmail: function () { - return typeof this.meta.letsencrypt_email !== 'undefined' ? this.meta.letsencrypt_email : App.Cache.User.get('email'); - }, - getLetsencryptAgree: function () { - return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:[^.]+\.?)+[^.]$/ - }); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.loader_content.hide(); - this.ui.le_error_info.hide(); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new CertificateModel.Model({provider: 'letsencrypt'}); - } - } -}); diff --git a/frontend/js/app/nginx/certificates/list/item.ejs b/frontend/js/app/nginx/certificates/list/item.ejs deleted file mode 100644 index 87930dce..00000000 --- a/frontend/js/app/nginx/certificates/list/item.ejs +++ /dev/null @@ -1,50 +0,0 @@ - -
- -
- - -
- <% - if (provider === 'letsencrypt') { - domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - } else { - %><%- nice_name %><% - } - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - - <%- i18n('ssl', provider) %><% if (meta.dns_provider) { %> - <%- dns_providers[meta.dns_provider].display_name %><% } %> - - - <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/certificates/list/item.js b/frontend/js/app/nginx/certificates/list/item.js deleted file mode 100644 index c967fdb8..00000000 --- a/frontend/js/app/nginx/certificates/list/item.js +++ /dev/null @@ -1,46 +0,0 @@ -const Mn = require('backbone.marionette'); -const moment = require('moment'); -const App = require('../../../main'); -const template = require('./item.ejs'); -const dns_providers = require('../../../../../../global/certbot-dns-plugins') - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - host_link: '.host-link', - renew: 'a.renew', - delete: 'a.delete' - }, - - events: { - 'click @ui.renew': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateRenew(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('certificates'), - isExpired: function () { - return moment(this.expires_on).isBefore(moment()); - }, - dns_providers: dns_providers - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/certificates/list/main.ejs b/frontend/js/app/nginx/certificates/list/main.ejs deleted file mode 100644 index aa49a27f..00000000 --- a/frontend/js/app/nginx/certificates/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('all-hosts', 'cert-provider') %> - <%- i18n('str', 'expires') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/certificates/list/main.js b/frontend/js/app/nginx/certificates/list/main.js deleted file mode 100644 index d96b43e8..00000000 --- a/frontend/js/app/nginx/certificates/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('certificates') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/certificates/main.ejs b/frontend/js/app/nginx/certificates/main.ejs deleted file mode 100644 index cc3624d5..00000000 --- a/frontend/js/app/nginx/certificates/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

<%- i18n('certificates', 'title') %>

-
- - <% if (showAddButton) { %> - - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/certificates/main.js b/frontend/js/app/nginx/certificates/main.js deleted file mode 100644 index e1148468..00000000 --- a/frontend/js/app/nginx/certificates/main.js +++ /dev/null @@ -1,82 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const CertificateModel = require('../../../models/certificate'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-certificates', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - let model = new CertificateModel.Model({provider: $(e.currentTarget).data('cert')}); - App.Controller.showNginxCertificateForm(model); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('certificates', 'help-title'), App.i18n('certificates', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('certificates') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.Certificates.getAll(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new CertificateModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('certificates'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('certificates', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('certificates', 'add') : null, - btn_color: 'pink', - permission: 'certificates', - action: function () { - App.Controller.showNginxCertificateForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxCertificates(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/certificates/renew.ejs b/frontend/js/app/nginx/certificates/renew.ejs deleted file mode 100644 index 4af186d0..00000000 --- a/frontend/js/app/nginx/certificates/renew.ejs +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/renew.js b/frontend/js/app/nginx/certificates/renew.js deleted file mode 100644 index 73632881..00000000 --- a/frontend/js/app/nginx/certificates/renew.js +++ /dev/null @@ -1,31 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./renew.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - waiting: '.waiting', - error: '.error', - close: 'button.cancel' - }, - - onRender: function () { - this.ui.error.hide(); - - App.Api.Nginx.Certificates.renew(this.model.get('id')) - .then((result) => { - this.model.set(result); - setTimeout(() => { - App.UI.closeModal(); - }, 1000); - }) - .catch((err) => { - this.ui.waiting.hide(); - this.ui.error.text(err.message).show(); - this.ui.close.prop('disabled', false); - }); - } -}); diff --git a/frontend/js/app/nginx/dead/delete.ejs b/frontend/js/app/nginx/dead/delete.ejs deleted file mode 100644 index cf720e86..00000000 --- a/frontend/js/app/nginx/dead/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/dead/delete.js b/frontend/js/app/nginx/dead/delete.js deleted file mode 100644 index d497d068..00000000 --- a/frontend/js/app/nginx/dead/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.DeadHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxDead(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/dead/form.ejs b/frontend/js/app/nginx/dead/form.ejs deleted file mode 100644 index 253c4b6f..00000000 --- a/frontend/js/app/nginx/dead/form.ejs +++ /dev/null @@ -1,206 +0,0 @@ - diff --git a/frontend/js/app/nginx/dead/form.js b/frontend/js/app/nginx/dead/form.js deleted file mode 100644 index 8f6774f6..00000000 --- a/frontend/js/app/nginx/dead/form.js +++ /dev/null @@ -1,286 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const DeadHostModel = require('../../../models/dead-host'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - letsencrypt: '.letsencrypt' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.http2_support = !!data.http2_support; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.DeadHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.DeadHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxDead(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new DeadHostModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/dead/list/item.ejs b/frontend/js/app/nginx/dead/list/item.ejs deleted file mode 100644 index d447bd1e..00000000 --- a/frontend/js/app/nginx/dead/list/item.ejs +++ /dev/null @@ -1,54 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/dead/list/item.js b/frontend/js/app/nginx/dead/list/item.js deleted file mode 100644 index a477dbfa..00000000 --- a/frontend/js/app/nginx/dead/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.DeadHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.DeadHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('dead_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/dead/list/main.ejs b/frontend/js/app/nginx/dead/list/main.ejs deleted file mode 100644 index e018a74b..00000000 --- a/frontend/js/app/nginx/dead/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/dead/list/main.js b/frontend/js/app/nginx/dead/list/main.js deleted file mode 100644 index 57931419..00000000 --- a/frontend/js/app/nginx/dead/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('dead_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/dead/main.ejs b/frontend/js/app/nginx/dead/main.ejs deleted file mode 100644 index 508280ae..00000000 --- a/frontend/js/app/nginx/dead/main.ejs +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-

<%- i18n('dead-hosts', 'title') %>

-
- - <% if (showAddButton) { %> - <%- i18n('dead-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/dead/main.js b/frontend/js/app/nginx/dead/main.js deleted file mode 100644 index ac3cb7f1..00000000 --- a/frontend/js/app/nginx/dead/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const DeadHostModel = require('../../../models/dead-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-dead', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxDeadForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('dead-hosts', 'help-title'), App.i18n('dead-hosts', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('dead_hosts') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.DeadHosts.getAll(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new DeadHostModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('dead_hosts'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('dead-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('dead-hosts', 'add') : null, - btn_color: 'danger', - permission: 'dead_hosts', - action: function () { - App.Controller.showNginxDeadForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxDead(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/proxy/access-list-item.ejs b/frontend/js/app/nginx/proxy/access-list-item.ejs deleted file mode 100644 index e5a7e116..00000000 --- a/frontend/js/app/nginx/proxy/access-list-item.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
- <% if (id > 0) { %> -
- <%- name %> -
- <%- i18n('access-lists', 'item-count', {count: items.length || 0}) %>, <%- i18n('access-lists', 'client-count', {count: clients.length || 0}) %> – Created: <%- formatDbDate(created_on, 'Do MMMM YYYY, h:mm a') %> - <% } else { %> -
- <%- i18n('access-lists', 'public') %> -
- <%- i18n('access-lists', 'public-sub') %> - <% } %> -
diff --git a/frontend/js/app/nginx/proxy/delete.ejs b/frontend/js/app/nginx/proxy/delete.ejs deleted file mode 100644 index 2fe099fa..00000000 --- a/frontend/js/app/nginx/proxy/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/proxy/delete.js b/frontend/js/app/nginx/proxy/delete.js deleted file mode 100644 index 63a8e020..00000000 --- a/frontend/js/app/nginx/proxy/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.ProxyHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxProxy(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs deleted file mode 100644 index 1a498301..00000000 --- a/frontend/js/app/nginx/proxy/form.ejs +++ /dev/null @@ -1,280 +0,0 @@ - diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js deleted file mode 100644 index 8802b958..00000000 --- a/frontend/js/app/nginx/proxy/form.js +++ /dev/null @@ -1,369 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const ProxyHostModel = require('../../../models/proxy-host'); -const ProxyLocationModel = require('../../../models/proxy-host-location'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const accessListItemTemplate = require('./access-list-item.ejs'); -const CustomLocation = require('./location'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - locationsCollection: new ProxyLocationModel.Collection(), - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - forward_host: 'input[name="forward_host"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - add_location_btn: 'button.add_location', - locations_container: '.locations_container', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - access_list_select: 'select[name="access_list_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - forward_scheme: 'select[name="forward_scheme"]', - letsencrypt: '.letsencrypt' - }, - - regions: { - locations_regions: '@ui.locations_container' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.add_location_btn': function (e) { - e.preventDefault(); - - const model = new ProxyLocationModel.Model(); - this.locationsCollection.add(model); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Add locations - data.locations = []; - this.locationsCollection.models.forEach((location) => { - data.locations.push(location.toJSON()); - }); - - // Serialize collects path from custom locations - // This field must be removed from root object - delete data.path; - - // Manipulate - data.forward_port = parseInt(data.forward_port, 10); - data.block_exploits = !!data.block_exploits; - data.caching_enabled = !!data.caching_enabled; - data.allow_websocket_upgrade = !!data.allow_websocket_upgrade; - data.http2_support = !!data.http2_support; - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.ProxyHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.ProxyHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxProxy(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - this.ui.ssl_forced.trigger('change'); - this.ui.hsts_enabled.trigger('change'); - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Access Lists - this.ui.access_list_select.selectize({ - valueField: 'id', - labelField: 'name', - searchField: ['name'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return accessListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.AccessLists.getAll(['items', 'clients']) - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id')); - } - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new ProxyHostModel.Model(); - } - - this.locationsCollection = new ProxyLocationModel.Collection(); - - // Custom locations - this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({ - collection: this.locationsCollection - })); - - // Check wether there are any location defined - if (options.model && Array.isArray(options.model.attributes.locations)) { - options.model.attributes.locations.forEach((location) => { - let m = new ProxyLocationModel.Model(location); - this.locationsCollection.add(m); - }); - } - } -}); diff --git a/frontend/js/app/nginx/proxy/list/item.ejs b/frontend/js/app/nginx/proxy/list/item.ejs deleted file mode 100644 index a5936804..00000000 --- a/frontend/js/app/nginx/proxy/list/item.ejs +++ /dev/null @@ -1,60 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forward_scheme %>://<%- forward_host %>:<%- forward_port %>
- - -
<%- certificate && certificate_id ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - -
<%- access_list_id ? access_list.name : i18n('str', 'public') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/proxy/list/item.js b/frontend/js/app/nginx/proxy/list/item.js deleted file mode 100644 index 37d199b4..00000000 --- a/frontend/js/app/nginx/proxy/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.ProxyHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.ProxyHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/proxy/list/main.ejs b/frontend/js/app/nginx/proxy/list/main.ejs deleted file mode 100644 index 6de5b9c6..00000000 --- a/frontend/js/app/nginx/proxy/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('str', 'destination') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'access') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/proxy/list/main.js b/frontend/js/app/nginx/proxy/list/main.js deleted file mode 100644 index 09e984e6..00000000 --- a/frontend/js/app/nginx/proxy/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/proxy/location-item.ejs b/frontend/js/app/nginx/proxy/location-item.ejs deleted file mode 100644 index 39445f7b..00000000 --- a/frontend/js/app/nginx/proxy/location-item.ejs +++ /dev/null @@ -1,64 +0,0 @@ -
-
-
-
-
- -
-
-
- - location - - -
-
-
-
- -
-
-
-
-
-
-
- - -
-
-
-
- - - <%- i18n('proxy-hosts', 'custom-forward-host-help') %> -
-
-
-
- - -
-
-
-
-
-
- -
-
-
- - - <%- i18n('locations', 'delete') %> - -
-
diff --git a/frontend/js/app/nginx/proxy/location.js b/frontend/js/app/nginx/proxy/location.js deleted file mode 100644 index e9513a48..00000000 --- a/frontend/js/app/nginx/proxy/location.js +++ /dev/null @@ -1,54 +0,0 @@ -const locationItemTemplate = require('./location-item.ejs'); -const Mn = require('backbone.marionette'); -const App = require('../../main'); - -const LocationView = Mn.View.extend({ - template: locationItemTemplate, - className: 'location_block', - - ui: { - toggle: 'input[type="checkbox"]', - config: '.config', - delete: '.location-delete' - }, - - events: { - 'change @ui.toggle': function(el) { - if (el.target.checked) { - this.ui.config.show(); - } else { - this.ui.config.hide(); - } - }, - - 'change .model': function (e) { - const map = {}; - map[e.target.name] = e.target.value; - this.model.set(map); - }, - - 'click @ui.delete': function () { - this.model.destroy(); - } - }, - - onRender: function() { - $(this.ui.config).hide(); - }, - - templateContext: function() { - return { - i18n: App.i18n - } - } -}); - -const LocationCollectionView = Mn.CollectionView.extend({ - className: 'locations_container', - childView: LocationView -}); - -module.exports = { - LocationCollectionView, - LocationView -} \ No newline at end of file diff --git a/frontend/js/app/nginx/proxy/main.ejs b/frontend/js/app/nginx/proxy/main.ejs deleted file mode 100644 index a5114de6..00000000 --- a/frontend/js/app/nginx/proxy/main.ejs +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-

<%- i18n('proxy-hosts', 'title') %>

-
- - <% if (showAddButton) { %> - <%- i18n('proxy-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/proxy/main.js b/frontend/js/app/nginx/proxy/main.js deleted file mode 100644 index 83cedfba..00000000 --- a/frontend/js/app/nginx/proxy/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const ProxyHostModel = require('../../../models/proxy-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-proxy', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxProxyForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('proxy-hosts', 'help-title'), App.i18n('proxy-hosts', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.ProxyHosts.getAll(['owner', 'access_list', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new ProxyHostModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('proxy_hosts'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('proxy-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('proxy-hosts', 'add') : null, - btn_color: 'success', - permission: 'proxy_hosts', - action: function () { - App.Controller.showNginxProxyForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxProxy(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/redirection/delete.ejs b/frontend/js/app/nginx/redirection/delete.ejs deleted file mode 100644 index 8353d1b7..00000000 --- a/frontend/js/app/nginx/redirection/delete.ejs +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/frontend/js/app/nginx/redirection/delete.js b/frontend/js/app/nginx/redirection/delete.js deleted file mode 100644 index 6d2862f6..00000000 --- a/frontend/js/app/nginx/redirection/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.RedirectionHosts.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxRedirection(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/redirection/form.ejs b/frontend/js/app/nginx/redirection/form.ejs deleted file mode 100644 index 7e190719..00000000 --- a/frontend/js/app/nginx/redirection/form.ejs +++ /dev/null @@ -1,253 +0,0 @@ - diff --git a/frontend/js/app/nginx/redirection/form.js b/frontend/js/app/nginx/redirection/form.js deleted file mode 100644 index 1f81feeb..00000000 --- a/frontend/js/app/nginx/redirection/form.js +++ /dev/null @@ -1,288 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const RedirectionHostModel = require('../../../models/redirection-host'); -const template = require('./form.ejs'); -const certListItemTemplate = require('../certificates-list-item.ejs'); -const Helpers = require('../../../lib/helpers'); -const i18n = require('../../i18n'); -const dns_providers = require('../../../../../global/certbot-dns-plugins'); - - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - letsencrypt: '.letsencrypt' - }, - - events: { - 'change @ui.certificate_select': function () { - let id = this.ui.certificate_select.val(); - if (id === 'new') { - this.ui.letsencrypt.show().find('input').prop('disabled', false); - this.ui.dns_challenge_content.hide(); - } else { - this.ui.letsencrypt.hide().find('input').prop('disabled', true); - } - - let enabled = id === 'new' || parseInt(id, 10) > 0; - - let inputs = this.ui.ssl_forced.add(this.ui.http2_support); - inputs - .prop('disabled', !enabled) - .parents('.form-group') - .css('opacity', enabled ? 1 : 0.5); - - if (!enabled) { - inputs.prop('checked', false); - } - - inputs.trigger('change'); - }, - - 'change @ui.ssl_forced': function () { - let checked = this.ui.ssl_forced.prop('checked'); - this.ui.hsts_enabled - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_enabled.prop('checked', false); - } - - this.ui.hsts_enabled.trigger('change'); - }, - - 'change @ui.hsts_enabled': function () { - let checked = this.ui.hsts_enabled.prop('checked'); - this.ui.hsts_subdomains - .prop('disabled', !checked) - .parents('.form-group') - .css('opacity', checked ? 1 : 0.5); - - if (!checked) { - this.ui.hsts_subdomains.prop('checked', false); - } - }, - - 'change @ui.dns_challenge_switch': function () { - const checked = this.ui.dns_challenge_switch.prop('checked'); - if (checked) { - this.ui.dns_provider.prop('required', 'required'); - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){ - this.ui.dns_provider_credentials.prop('required', 'required'); - } - this.ui.dns_challenge_content.show(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - } - }, - - 'change @ui.dns_provider': function () { - const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value; - if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) { - this.ui.dns_provider_credentials.prop('required', 'required'); - this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials; - this.ui.credentials_file_content.show(); - } else { - this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); - } - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.le_error_info.hide(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - data.block_exploits = !!data.block_exploits; - data.preserve_path = !!data.preserve_path; - data.http2_support = !!data.http2_support; - data.hsts_enabled = !!data.hsts_enabled; - data.hsts_subdomains = !!data.hsts_subdomains; - data.ssl_forced = !!data.ssl_forced; - - if (typeof data.meta === 'undefined') data.meta = {}; - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; - data.meta.dns_challenge = data.meta.dns_challenge == 1; - - if(!data.meta.dns_challenge){ - data.meta.dns_provider = undefined; - data.meta.dns_provider_credentials = undefined; - data.meta.propagation_seconds = undefined; - } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; - } - - if (typeof data.domain_names === 'string' && data.domain_names) { - data.domain_names = data.domain_names.split(','); - } - - // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { - let domain_err = false; - if (!data.meta.dns_challenge) { - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); - } - - if (domain_err) { - alert(i18n('ssl', 'no-wildcard-without-dns')); - return; - } - } else { - data.certificate_id = parseInt(data.certificate_id, 10); - } - - let method = App.Api.Nginx.RedirectionHosts.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.RedirectionHosts.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - this.ui.save.addClass('btn-loading'); - - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxRedirection(); - } - }); - }) - .catch(err => { - let more_info = ''; - if(err.code === 500 && err.debug){ - try{ - more_info = JSON.parse(err.debug).debug.stack.join("\n"); - } catch(e) {} - } - this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `
${more_info}
`:''}`; - this.ui.le_error_info.show(); - this.ui.le_error_info[0].scrollIntoView(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - this.ui.save.removeClass('btn-loading'); - }); - } - }, - - templateContext: { - getLetsencryptEmail: function () { - return App.Cache.User.get('email'); - }, - getUseDnsChallenge: function () { - return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false; - }, - getDnsProvider: function () { - return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null; - }, - getDnsProviderCredentials: function () { - return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : ''; - }, - getPropagationSeconds: function () { - return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : ''; - }, - dns_plugins: dns_providers, - }, - - onRender: function () { - let view = this; - - // Domain names - this.ui.domain_names.selectize({ - delimiter: ',', - persist: false, - maxOptions: 15, - create: function (input) { - return { - value: input, - text: input - }; - }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ - }); - - // Certificates - this.ui.le_error_info.hide(); - this.ui.dns_challenge_content.hide(); - this.ui.credentials_file_content.hide(); - this.ui.letsencrypt.hide(); - this.ui.certificate_select.selectize({ - valueField: 'id', - labelField: 'nice_name', - searchField: ['nice_name', 'domain_names'], - create: false, - preload: true, - allowEmptyOption: true, - render: { - option: function (item) { - item.i18n = App.i18n; - item.formatDbDate = Helpers.formatDbDate; - return certListItemTemplate(item); - } - }, - load: function (query, callback) { - App.Api.Nginx.Certificates.getAll() - .then(rows => { - callback(rows); - }) - .catch(err => { - console.error(err); - callback(); - }); - }, - onLoad: function () { - view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); - } - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new RedirectionHostModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/redirection/list/item.ejs b/frontend/js/app/nginx/redirection/list/item.ejs deleted file mode 100644 index 4f25d973..00000000 --- a/frontend/js/app/nginx/redirection/list/item.ejs +++ /dev/null @@ -1,63 +0,0 @@ - -
- -
- - -
- <% domain_names.map(function(host) { - if (host.indexOf('*') === -1) { - %> - <%- host %> - <% - } else { - %> - <%- host %> - <% - } - }); - %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forward_http_code %>
- - -
<%- forward_scheme == '$scheme' ? 'auto' : forward_scheme %>
- - -
<%- forward_domain_name %>
- - -
<%- certificate ? i18n('ssl', certificate.provider) : i18n('ssl', 'none') %>
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> diff --git a/frontend/js/app/nginx/redirection/list/item.js b/frontend/js/app/nginx/redirection/list/item.js deleted file mode 100644 index 05adc251..00000000 --- a/frontend/js/app/nginx/redirection/list/item.js +++ /dev/null @@ -1,61 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete', - host_link: '.host-link' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.RedirectionHosts[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.RedirectionHosts.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionDeleteConfirm(this.model); - }, - - 'click @ui.host_link': function (e) { - e.preventDefault(); - let win = window.open($(e.currentTarget).attr('rel'), '_blank'); - win.focus(); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('redirection_hosts'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/redirection/list/main.ejs b/frontend/js/app/nginx/redirection/list/main.ejs deleted file mode 100644 index 8b6930d6..00000000 --- a/frontend/js/app/nginx/redirection/list/main.ejs +++ /dev/null @@ -1,15 +0,0 @@ - -   - <%- i18n('str', 'source') %> - <%- i18n('redirection-hosts', 'forward-http-status-code') %> - <%- i18n('redirection-hosts', 'forward-scheme') %> - <%- i18n('str', 'destination') %> - <%- i18n('str', 'ssl') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/redirection/list/main.js b/frontend/js/app/nginx/redirection/list/main.js deleted file mode 100644 index d368cf6a..00000000 --- a/frontend/js/app/nginx/redirection/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('redirection_hosts') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/redirection/main.ejs b/frontend/js/app/nginx/redirection/main.ejs deleted file mode 100644 index 4345a7e8..00000000 --- a/frontend/js/app/nginx/redirection/main.ejs +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-

Redirection Hosts

-
- - <% if (showAddButton) { %> - Add Redirection Host - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/redirection/main.js b/frontend/js/app/nginx/redirection/main.js deleted file mode 100644 index f45f9a07..00000000 --- a/frontend/js/app/nginx/redirection/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const RedirectionHostModel = require('../../../models/redirection-host'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-redirection', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxRedirectionForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('redirection-hosts', 'help-title'), App.i18n('redirection-hosts', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.RedirectionHosts.getAll(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new RedirectionHostModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('redirection_hosts'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('redirection-hosts', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('redirection-hosts', 'add') : null, - btn_color: 'yellow', - permission: 'redirection_hosts', - action: function () { - App.Controller.showNginxRedirectionForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxRedirection(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/nginx/stream/delete.ejs b/frontend/js/app/nginx/stream/delete.ejs deleted file mode 100644 index d7ba3a21..00000000 --- a/frontend/js/app/nginx/stream/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/nginx/stream/delete.js b/frontend/js/app/nginx/stream/delete.js deleted file mode 100644 index 71eff18c..00000000 --- a/frontend/js/app/nginx/stream/delete.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./delete.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Nginx.Streams.delete(this.model.get('id')) - .then(() => { - App.Controller.showNginxStream(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/nginx/stream/form.ejs b/frontend/js/app/nginx/stream/form.ejs deleted file mode 100644 index b0a72e48..00000000 --- a/frontend/js/app/nginx/stream/form.ejs +++ /dev/null @@ -1,55 +0,0 @@ - diff --git a/frontend/js/app/nginx/stream/form.js b/frontend/js/app/nginx/stream/form.js deleted file mode 100644 index 2133c3da..00000000 --- a/frontend/js/app/nginx/stream/form.js +++ /dev/null @@ -1,91 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const StreamModel = require('../../../models/stream'); -const template = require('./form.ejs'); - -require('jquery-serializejson'); -require('jquery-mask-plugin'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - forward_ip: 'input[name="forward_ip"]', - type_error: '.forward-type-error', - buttons: '.modal-footer button', - switches: '.custom-switch-input', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - 'change @ui.switches': function () { - this.ui.type_error.hide(); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - - if (!data.tcp_forwarding && !data.udp_forwarding) { - this.ui.type_error.show(); - return; - } - - // Manipulate - data.incoming_port = parseInt(data.incoming_port, 10); - data.forwarding_port = parseInt(data.forwarding_port, 10); - data.tcp_forwarding = !!data.tcp_forwarding; - data.udp_forwarding = !!data.udp_forwarding; - - let method = App.Api.Nginx.Streams.create; - let is_new = true; - - if (this.model.get('id')) { - // edit - is_new = false; - method = App.Api.Nginx.Streams.update; - data.id = this.model.get('id'); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - method(data) - .then(result => { - view.model.set(result); - - App.UI.closeModal(function () { - if (is_new) { - App.Controller.showNginxStream(); - } - }); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - onRender: function () { - this.ui.forward_ip.mask('099.099.099.099', { - clearIfNotMatch: true, - placeholder: '000.000.000.000' - }); - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new StreamModel.Model(); - } - } -}); diff --git a/frontend/js/app/nginx/stream/list/item.ejs b/frontend/js/app/nginx/stream/list/item.ejs deleted file mode 100644 index 2c04667f..00000000 --- a/frontend/js/app/nginx/stream/list/item.ejs +++ /dev/null @@ -1,53 +0,0 @@ - -
- -
- - -
- <%- incoming_port %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- forward_ip %>:<%- forwarding_port %>
- - -
- <% if (tcp_forwarding) { %> - <%- i18n('streams', 'tcp') %> - <% } - if (udp_forwarding) { %> - <%- i18n('streams', 'udp') %> - <% } %> -
- - - <% - var o = isOnline(); - if (!enabled) { %> - <%- i18n('str', 'disabled') %> - <% } else if (o === true) { %> - <%- i18n('str', 'online') %> - <% } else if (o === false) { %> - <%- i18n('str', 'offline') %> - <% } else { %> - <%- i18n('str', 'unknown') %> - <% } %> - -<% if (canManage) { %> - - - -<% } %> \ No newline at end of file diff --git a/frontend/js/app/nginx/stream/list/item.js b/frontend/js/app/nginx/stream/list/item.js deleted file mode 100644 index a6892ee2..00000000 --- a/frontend/js/app/nginx/stream/list/item.js +++ /dev/null @@ -1,54 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - able: 'a.able', - edit: 'a.edit', - delete: 'a.delete' - }, - - events: { - 'click @ui.able': function (e) { - e.preventDefault(); - let id = this.model.get('id'); - App.Api.Nginx.Streams[this.model.get('enabled') ? 'disable' : 'enable'](id) - .then(() => { - return App.Api.Nginx.Streams.get(id) - .then(row => { - this.model.set(row); - }); - }); - }, - - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamDeleteConfirm(this.model); - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('streams'), - - isOnline: function () { - return typeof this.meta.nginx_online === 'undefined' ? null : this.meta.nginx_online; - }, - - getOfflineError: function () { - return this.meta.nginx_err || ''; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/nginx/stream/list/main.ejs b/frontend/js/app/nginx/stream/list/main.ejs deleted file mode 100644 index 5304f614..00000000 --- a/frontend/js/app/nginx/stream/list/main.ejs +++ /dev/null @@ -1,13 +0,0 @@ - -   - <%- i18n('streams', 'incoming-port') %> - <%- i18n('str', 'destination') %> - <%- i18n('streams', 'protocol') %> - <%- i18n('str', 'status') %> - <% if (canManage) { %> -   - <% } %> - - - - diff --git a/frontend/js/app/nginx/stream/list/main.js b/frontend/js/app/nginx/stream/list/main.js deleted file mode 100644 index 36be621d..00000000 --- a/frontend/js/app/nginx/stream/list/main.js +++ /dev/null @@ -1,32 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../../main'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - templateContext: { - canManage: App.Cache.User.canManage('streams') - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/nginx/stream/main.ejs b/frontend/js/app/nginx/stream/main.ejs deleted file mode 100644 index c01414ce..00000000 --- a/frontend/js/app/nginx/stream/main.ejs +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-

<%- i18n('streams', 'title') %>

-
- - <% if (showAddButton) { %> - <%- i18n('streams', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/stream/main.js b/frontend/js/app/nginx/stream/main.js deleted file mode 100644 index a8eda92c..00000000 --- a/frontend/js/app/nginx/stream/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const StreamModel = require('../../../models/stream'); -const ListView = require('./list/main'); -const ErrorView = require('../../error/main'); -const EmptyView = require('../../empty/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'nginx-stream', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - help: '.help', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showNginxStreamForm(); - }, - - 'click @ui.help': function (e) { - e.preventDefault(); - App.Controller.showHelp(App.i18n('streams', 'help-title'), App.i18n('streams', 'help-content')); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('streams') - }, - - onRender: function () { - let view = this; - - App.Api.Nginx.Streams.getAll(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new StreamModel.Collection(response) - })); - } else { - let manage = App.Cache.User.canManage('streams'); - - view.showChildView('list_region', new EmptyView({ - title: App.i18n('streams', 'empty'), - subtitle: App.i18n('all-hosts', 'empty-subtitle', {manage: manage}), - link: manage ? App.i18n('streams', 'add') : null, - btn_color: 'blue', - permission: 'streams', - action: function () { - App.Controller.showNginxStreamForm(); - } - })); - } - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxStream(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/router.js b/frontend/js/app/router.js deleted file mode 100644 index a036bfc5..00000000 --- a/frontend/js/app/router.js +++ /dev/null @@ -1,19 +0,0 @@ -const AppRouter = require('marionette.approuter'); -const Controller = require('./controller'); - -module.exports = AppRouter.default.extend({ - controller: Controller, - appRoutes: { - users: 'showUsers', - logout: 'logout', - 'nginx/proxy': 'showNginxProxy', - 'nginx/redirection': 'showNginxRedirection', - 'nginx/404': 'showNginxDead', - 'nginx/stream': 'showNginxStream', - 'nginx/access': 'showNginxAccess', - 'nginx/certificates': 'showNginxCertificates', - 'audit-log': 'showAuditLog', - 'settings': 'showSettings', - '*default': 'showDashboard' - } -}); diff --git a/frontend/js/app/settings/default-site/main.ejs b/frontend/js/app/settings/default-site/main.ejs deleted file mode 100644 index 126c9d0a..00000000 --- a/frontend/js/app/settings/default-site/main.ejs +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/frontend/js/app/settings/default-site/main.js b/frontend/js/app/settings/default-site/main.js deleted file mode 100644 index 06a45b8b..00000000 --- a/frontend/js/app/settings/default-site/main.js +++ /dev/null @@ -1,69 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./main.ejs'); - -require('jquery-serializejson'); -require('selectize'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - options: '.option-item', - value: 'input[name="value"]', - redirect: '.redirect-input', - html: '.html-content' - }, - - events: { - 'change @ui.value': function (e) { - let val = this.ui.value.filter(':checked').val(); - this.ui.options.hide(); - this.ui.options.filter('.option-' + val).show(); - }, - - 'click @ui.save': function (e) { - e.preventDefault(); - - let val = this.ui.value.filter(':checked').val(); - - // Clear redirect field before validation - if (val !== 'redirect') { - this.ui.redirect.val('').attr('required', false); - } else { - this.ui.redirect.attr('required', true); - } - - this.ui.html.attr('required', val === 'html'); - - if (!this.ui.form[0].checkValidity()) { - $('').hide().appendTo(this.ui.form).click().remove(); - return; - } - - let view = this; - let data = this.ui.form.serializeJSON(); - data.id = this.model.get('id'); - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - App.Api.Settings.update(data) - .then(result => { - view.model.set(result); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - onRender: function () { - this.ui.value.trigger('change'); - } -}); diff --git a/frontend/js/app/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs deleted file mode 100644 index 4f81b450..00000000 --- a/frontend/js/app/settings/list/item.ejs +++ /dev/null @@ -1,21 +0,0 @@ - -
<%- name %>
-
- <%- description %> -
- - -
- <% if (id === 'default-site') { %> - <%- i18n('settings', 'default-site-' + value) %> - <% } %> -
- - - - \ No newline at end of file diff --git a/frontend/js/app/settings/list/item.js b/frontend/js/app/settings/list/item.js deleted file mode 100644 index 03f9ac05..00000000 --- a/frontend/js/app/settings/list/item.js +++ /dev/null @@ -1,23 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showSettingForm(this.model); - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/settings/list/main.ejs b/frontend/js/app/settings/list/main.ejs deleted file mode 100644 index c96e923a..00000000 --- a/frontend/js/app/settings/list/main.ejs +++ /dev/null @@ -1,8 +0,0 @@ - - <%- i18n('str', 'name') %> - <%- i18n('str', 'value') %> -   - - - - diff --git a/frontend/js/app/settings/list/main.js b/frontend/js/app/settings/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/settings/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/settings/main.ejs b/frontend/js/app/settings/main.ejs deleted file mode 100644 index 2b02769f..00000000 --- a/frontend/js/app/settings/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
-

<%- i18n('settings', 'title') %>

-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/settings/main.js b/frontend/js/app/settings/main.js deleted file mode 100644 index 96b2941f..00000000 --- a/frontend/js/app/settings/main.js +++ /dev/null @@ -1,48 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const SettingModel = require('../../models/setting'); -const ListView = require('./list/main'); -const ErrorView = require('../error/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'settings', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - onRender: function () { - let view = this; - - App.Api.Settings.getAll() - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new SettingModel.Collection(response) - })); - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showSettings(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/app/tokens.js b/frontend/js/app/tokens.js deleted file mode 100644 index 4a56bcab..00000000 --- a/frontend/js/app/tokens.js +++ /dev/null @@ -1,126 +0,0 @@ -const STORAGE_NAME = 'nginx-proxy-manager-tokens'; - -/** - * @returns {Array} - */ -const getStorageTokens = function () { - let json = window.localStorage.getItem(STORAGE_NAME); - if (json) { - try { - return JSON.parse(json); - } catch (err) { - return []; - } - } - - return []; -}; - -/** - * @param {Array} tokens - */ -const setStorageTokens = function (tokens) { - window.localStorage.setItem(STORAGE_NAME, JSON.stringify(tokens)); -}; - -const Tokens = { - - /** - * @returns {Number} - */ - getTokenCount: () => { - return getStorageTokens().length; - }, - - /** - * @returns {Object} t,n - */ - getTopToken: () => { - let tokens = getStorageTokens(); - if (tokens && tokens.length) { - return tokens[0]; - } - - return null; - }, - - /** - * @returns {String} - */ - getNextTokenName: () => { - let tokens = getStorageTokens(); - if (tokens && tokens.length > 1 && typeof tokens[1] !== 'undefined' && typeof tokens[1].n !== 'undefined') { - return tokens[1].n; - } - - return null; - }, - - /** - * - * @param {String} token - * @param {String} [name] - * @returns {Number} - */ - addToken: (token, name) => { - // Get top token and if it's the same, ignore this call - let top = Tokens.getTopToken(); - if (!top || top.t !== token) { - let tokens = getStorageTokens(); - tokens.unshift({t: token, n: name || null}); - setStorageTokens(tokens); - } - - return Tokens.getTokenCount(); - }, - - /** - * @param {String} token - * @returns {Boolean} - */ - setCurrentToken: token => { - let tokens = getStorageTokens(); - if (tokens.length) { - tokens[0].t = token; - setStorageTokens(tokens); - return true; - } - - return false; - }, - - /** - * @param {String} name - * @returns {Boolean} - */ - setCurrentName: name => { - let tokens = getStorageTokens(); - if (tokens.length) { - tokens[0].n = name; - setStorageTokens(tokens); - return true; - } - - return false; - }, - - /** - * @returns {Number} - */ - dropTopToken: () => { - let tokens = getStorageTokens(); - tokens.shift(); - setStorageTokens(tokens); - return tokens.length; - }, - - /** - * - */ - clearTokens: () => { - window.localStorage.removeItem(STORAGE_NAME); - } - -}; - -module.exports = Tokens; diff --git a/frontend/js/app/ui/footer/main.ejs b/frontend/js/app/ui/footer/main.ejs deleted file mode 100644 index 562e71c2..00000000 --- a/frontend/js/app/ui/footer/main.ejs +++ /dev/null @@ -1,16 +0,0 @@ -
- -
- <%- i18n('main', 'version', {version: getVersion()}) %> - <%= i18n('footer', 'copy', {url: 'https://jc21.com?utm_source=nginx-proxy-manager'}) %> - <%= i18n('footer', 'theme', {url: 'https://tabler.github.io/?utm_source=nginx-proxy-manager'}) %> -
-
diff --git a/frontend/js/app/ui/footer/main.js b/frontend/js/app/ui/footer/main.js deleted file mode 100644 index 73f515e6..00000000 --- a/frontend/js/app/ui/footer/main.js +++ /dev/null @@ -1,14 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); -const Cache = require('../../cache'); - -module.exports = Mn.View.extend({ - className: 'container', - template: template, - - templateContext: { - getVersion: function () { - return Cache.version || '0.0.0'; - } - } -}); diff --git a/frontend/js/app/ui/header/main.ejs b/frontend/js/app/ui/header/main.ejs deleted file mode 100644 index 5f8a6278..00000000 --- a/frontend/js/app/ui/header/main.ejs +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/frontend/js/app/ui/header/main.js b/frontend/js/app/ui/header/main.js deleted file mode 100644 index 9779b45c..00000000 --- a/frontend/js/app/ui/header/main.js +++ /dev/null @@ -1,67 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const i18n = require('../../i18n'); -const Cache = require('../../cache'); -const Controller = require('../../controller'); -const Tokens = require('../../tokens'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'header', - className: 'header', - template: template, - - ui: { - link: 'a', - details: 'a.edit-details', - password: 'a.change-password' - }, - - events: { - 'click @ui.details': function (e) { - e.preventDefault(); - Controller.showUserForm(Cache.User); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - Controller.showUserPasswordForm(Cache.User); - }, - - 'click @ui.link': function (e) { - e.preventDefault(); - let href = $(e.currentTarget).attr('href'); - - switch (href) { - case '/': - Controller.showDashboard(); - break; - case '/logout': - Controller.logout(); - break; - } - } - }, - - templateContext: { - getUserField: function (field, default_val) { - return Cache.User.get(field) || default_val; - }, - - getRole: function () { - return i18n('roles', Cache.User.isAdmin() ? 'admin' : 'user'); - }, - - getLogoutText: function () { - if (Tokens.getTokenCount() > 1) { - return i18n('main', 'sign-in-as', {name: Tokens.getNextTokenName()}); - } - - return i18n('str', 'sign-out'); - } - }, - - initialize: function () { - this.listenTo(Cache.User, 'change', this.render); - } -}); diff --git a/frontend/js/app/ui/main.ejs b/frontend/js/app/ui/main.ejs deleted file mode 100644 index 7c97cf7d..00000000 --- a/frontend/js/app/ui/main.ejs +++ /dev/null @@ -1,19 +0,0 @@ -
- - -
-
- -
-
-
- -
- -
- - diff --git a/frontend/js/app/ui/main.js b/frontend/js/app/ui/main.js deleted file mode 100644 index c90c61d5..00000000 --- a/frontend/js/app/ui/main.js +++ /dev/null @@ -1,98 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./main.ejs'); -const HeaderView = require('./header/main'); -const MenuView = require('./menu/main'); -const FooterView = require('./footer/main'); -const Cache = require('../cache'); - -module.exports = Mn.View.extend({ - id: 'app', - className: 'page', - template: template, - modal_setup: false, - - modal: null, - - ui: { - modal: '#modal-dialog' - }, - - regions: { - header_region: { - el: '#header', - replaceElement: true - }, - menu_region: { - el: '#menu', - replaceElement: true - }, - footer_region: '.footer', - app_content_region: '#app-content', - modal_region: '#modal-dialog' - }, - - /** - * @param {Object} view - */ - showAppContent: function (view) { - this.showChildView('app_content_region', view); - }, - - /** - * @param {Object} view - * @param {Function} [show_callback] - * @param {Function} [shown_callback] - */ - showModalDialog: function (view, show_callback, shown_callback) { - this.showChildView('modal_region', view); - let modal = this.getRegion('modal_region').$el.modal('show'); - - modal.on('hidden.bs.modal', function (/*e*/) { - if (show_callback) { - modal.off('show.bs.modal', show_callback); - } - - if (shown_callback) { - modal.off('shown.bs.modal', shown_callback); - } - - modal.off('hidden.bs.modal'); - view.destroy(); - }); - - if (show_callback) { - modal.on('show.bs.modal', show_callback); - } - - if (shown_callback) { - modal.on('shown.bs.modal', shown_callback); - } - }, - - /** - * - * @param {Function} [hidden_callback] - */ - closeModal: function (hidden_callback) { - let modal = this.getRegion('modal_region').$el.modal('hide'); - - if (hidden_callback) { - modal.on('hidden.bs.modal', hidden_callback); - } - }, - - onRender: function () { - this.showChildView('header_region', new HeaderView({ - model: Cache.User - })); - - this.showChildView('menu_region', new MenuView()); - this.showChildView('footer_region', new FooterView()); - }, - - reset: function () { - this.getRegion('header_region').reset(); - this.getRegion('footer_region').reset(); - this.getRegion('modal_region').reset(); - } -}); diff --git a/frontend/js/app/ui/menu/main.ejs b/frontend/js/app/ui/menu/main.ejs deleted file mode 100644 index 671b4e3b..00000000 --- a/frontend/js/app/ui/menu/main.ejs +++ /dev/null @@ -1,52 +0,0 @@ -
-
-
- -
-
-
diff --git a/frontend/js/app/ui/menu/main.js b/frontend/js/app/ui/menu/main.js deleted file mode 100644 index dabe26d3..00000000 --- a/frontend/js/app/ui/menu/main.js +++ /dev/null @@ -1,39 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const Controller = require('../../controller'); -const Cache = require('../../cache'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'menu', - className: 'header collapse d-lg-flex p-0', - template: template, - - ui: { - links: 'a' - }, - - events: { - 'click @ui.links': function (e) { - let href = $(e.currentTarget).attr('href'); - if (href !== '#') { - e.preventDefault(); - Controller.navigate(href, true); - } - } - }, - - templateContext: { - isAdmin: function () { - return Cache.User.isAdmin(); - }, - - canShow: function (perm) { - return Cache.User.isAdmin() || Cache.User.canView(perm); - } - }, - - initialize: function () { - this.listenTo(Cache.User, 'change', this.render); - } -}); diff --git a/frontend/js/app/user/delete.ejs b/frontend/js/app/user/delete.ejs deleted file mode 100644 index 484e2786..00000000 --- a/frontend/js/app/user/delete.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/frontend/js/app/user/delete.js b/frontend/js/app/user/delete.js deleted file mode 100644 index e8ed5c32..00000000 --- a/frontend/js/app/user/delete.js +++ /dev/null @@ -1,34 +0,0 @@ -const Mn = require('backbone.marionette'); -const template = require('./delete.ejs'); -const App = require('../main'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - App.Api.Users.delete(this.model.get('id')) - .then(() => { - App.Controller.showUsers(); - App.UI.closeModal(); - }) - .catch(err => { - alert(err.message); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } -}); diff --git a/frontend/js/app/user/form.ejs b/frontend/js/app/user/form.ejs deleted file mode 100644 index aeb268f7..00000000 --- a/frontend/js/app/user/form.ejs +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/frontend/js/app/user/form.js b/frontend/js/app/user/form.js deleted file mode 100644 index ef92ec3e..00000000 --- a/frontend/js/app/user/form.js +++ /dev/null @@ -1,108 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const template = require('./form.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - error: '.secret-error' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.error.hide(); - let view = this; - let data = this.ui.form.serializeJSON(); - - let show_password = this.model.get('email') === 'admin@example.com'; - - // admin@example.com is not allowed - if (data.email === 'admin@example.com') { - this.ui.error.text(App.i18n('users', 'default_error')).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - return; - } - - // Manipulate - data.roles = []; - if ((this.model.get('id') === App.Cache.User.get('id') && this.model.isAdmin()) || (typeof data.is_admin !== 'undefined' && data.is_admin)) { - data.roles.push('admin'); - delete data.is_admin; - } - - data.is_disabled = typeof data.is_disabled !== 'undefined' ? !!data.is_disabled : false; - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - let method = App.Api.Users.create; - - if (this.model.get('id')) { - // edit - method = App.Api.Users.update; - data.id = this.model.get('id'); - } - - method(data) - .then(result => { - if (result.id === App.Cache.User.get('id')) { - App.Cache.User.set(result); - } - - if (view.model.get('id') !== App.Cache.User.get('id')) { - App.Controller.showUsers(); - } - - view.model.set(result); - App.UI.closeModal(function () { - if (method === App.Api.Users.create) { - // Show permissions dialog immediately - App.Controller.showUserPermissions(view.model); - } else if (show_password) { - App.Controller.showUserPasswordForm(view.model); - } - }); - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - templateContext: function () { - let view = this; - - return { - isSelf: function () { - return view.model.get('id') === App.Cache.User.get('id'); - }, - - isAdmin: function () { - return App.Cache.User.isAdmin(); - }, - - isAdminUser: function () { - return view.model.isAdmin(); - }, - - isDisabled: function () { - return view.model.isDisabled(); - } - }; - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new UserModel.Model(); - } - } -}); diff --git a/frontend/js/app/user/password.ejs b/frontend/js/app/user/password.ejs deleted file mode 100644 index a45cc7ed..00000000 --- a/frontend/js/app/user/password.ejs +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/frontend/js/app/user/password.js b/frontend/js/app/user/password.js deleted file mode 100644 index 84030750..00000000 --- a/frontend/js/app/user/password.js +++ /dev/null @@ -1,69 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const template = require('./password.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - newSecretError: '.new-secret-error', - generalError: '#error-info', - }, - - events: { - 'click @ui.save': function (e) { - e.preventDefault(); - this.ui.newSecretError.hide(); - this.ui.generalError.hide(); - let form = this.ui.form.serializeJSON(); - - if (form.new_password1 !== form.new_password2) { - this.ui.newSecretError.text('Passwords do not match!').show(); - return; - } - - let data = { - type: 'password', - current: form.current_password, - secret: form.new_password1 - }; - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - App.Api.Users.setPassword(this.model.get('id'), data) - .then(() => { - App.UI.closeModal(); - App.Controller.showUsers(); - }) - .catch(err => { - // Change error message to make it a little clearer - if (err.message === 'Invalid password') { - err.message = 'Current password is invalid'; - } - this.ui.generalError.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - isSelf: function () { - return App.Cache.User.get('id') === this.model.get('id'); - }, - - templateContext: function () { - return { - isSelf: this.isSelf.bind(this) - }; - }, - - onRender: function () { - this.ui.newSecretError.hide(); - this.ui.generalError.hide(); - }, -}); diff --git a/frontend/js/app/user/permissions.ejs b/frontend/js/app/user/permissions.ejs deleted file mode 100644 index b6161796..00000000 --- a/frontend/js/app/user/permissions.ejs +++ /dev/null @@ -1,68 +0,0 @@ - diff --git a/frontend/js/app/user/permissions.js b/frontend/js/app/user/permissions.js deleted file mode 100644 index af8049ce..00000000 --- a/frontend/js/app/user/permissions.js +++ /dev/null @@ -1,95 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const template = require('./permissions.ejs'); - -require('jquery-serializejson'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - error: '.secret-error' - }, - - events: { - - 'click @ui.save': function (e) { - e.preventDefault(); - - let view = this; - let data = this.ui.form.serializeJSON(); - - // Manipulate - if (view.model.isAdmin()) { - // Force some attributes for admin - data = _.assign({}, data, { - access_lists: 'manage', - dead_hosts: 'manage', - proxy_hosts: 'manage', - redirection_hosts: 'manage', - streams: 'manage', - certificates: 'manage' - }); - } - - this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Users.setPermissions(view.model.get('id'), data) - .then(() => { - if (view.model.get('id') === App.Cache.User.get('id')) { - App.Cache.User.set({permissions: data}); - } - - view.model.set({permissions: data}); - App.UI.closeModal(); - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); - }); - } - }, - - templateContext: function () { - let perms = this.model.get('permissions'); - let is_admin = this.model.isAdmin(); - - return { - getPerm: function (key) { - if (perms !== null && typeof perms[key] !== 'undefined') { - return perms[key]; - } - - return null; - }, - - getPermProps: function (key, item, forced_admin) { - if (forced_admin && is_admin) { - return 'checked disabled'; - } else if (is_admin) { - return 'disabled'; - } else if (perms !== null && typeof perms[key] !== 'undefined' && perms[key] === item) { - return 'checked'; - } - - return ''; - }, - - isAdmin: function () { - return is_admin; - } - }; - }, - - initialize: function (options) { - if (typeof options.model === 'undefined' || !options.model) { - this.model = new UserModel.Model(); - } - } -}); diff --git a/frontend/js/app/users/list/item.ejs b/frontend/js/app/users/list/item.ejs deleted file mode 100644 index fab5585b..00000000 --- a/frontend/js/app/users/list/item.ejs +++ /dev/null @@ -1,45 +0,0 @@ - -
- -
- - -
<%- name %>
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
- - -
<%- email %>
- - -
- <% - var r = []; - roles.map(function(role) { - if (role) { - r.push(i18n('roles', role)); - } - }); - %> - <%- r.join(', ') %> -
- - - - diff --git a/frontend/js/app/users/list/item.js b/frontend/js/app/users/list/item.js deleted file mode 100644 index 4645a5c4..00000000 --- a/frontend/js/app/users/list/item.js +++ /dev/null @@ -1,68 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const Tokens = require('../../tokens'); -const template = require('./item.ejs'); - -module.exports = Mn.View.extend({ - template: template, - tagName: 'tr', - - ui: { - edit: 'a.edit-user', - permissions: 'a.edit-permissions', - password: 'a.set-password', - login: 'a.login', - delete: 'a.delete-user' - }, - - events: { - 'click @ui.edit': function (e) { - e.preventDefault(); - App.Controller.showUserForm(this.model); - }, - - 'click @ui.permissions': function (e) { - e.preventDefault(); - App.Controller.showUserPermissions(this.model); - }, - - 'click @ui.password': function (e) { - e.preventDefault(); - App.Controller.showUserPasswordForm(this.model); - }, - - 'click @ui.delete': function (e) { - e.preventDefault(); - App.Controller.showUserDeleteConfirm(this.model); - }, - - 'click @ui.login': function (e) { - e.preventDefault(); - - if (App.Cache.User.get('id') !== this.model.get('id')) { - this.ui.login.prop('disabled', true).addClass('btn-disabled'); - - App.Api.Users.loginAs(this.model.get('id')) - .then(res => { - Tokens.addToken(res.token, res.user.nickname || res.user.name); - window.location = '/'; - window.location.reload(); - }) - .catch(err => { - alert(err.message); - this.ui.login.prop('disabled', false).removeClass('btn-disabled'); - }); - } - } - }, - - templateContext: { - isSelf: function () { - return App.Cache.User.get('id') === this.id; - } - }, - - initialize: function () { - this.listenTo(this.model, 'change', this.render); - } -}); diff --git a/frontend/js/app/users/list/main.ejs b/frontend/js/app/users/list/main.ejs deleted file mode 100644 index c85c9cb1..00000000 --- a/frontend/js/app/users/list/main.ejs +++ /dev/null @@ -1,10 +0,0 @@ - -   - <%- i18n('str', 'name') %> - <%- i18n('str', 'email') %> - <%- i18n('str', 'roles') %> -   - - - - diff --git a/frontend/js/app/users/list/main.js b/frontend/js/app/users/list/main.js deleted file mode 100644 index 9d3e26fb..00000000 --- a/frontend/js/app/users/list/main.js +++ /dev/null @@ -1,27 +0,0 @@ -const Mn = require('backbone.marionette'); -const ItemView = require('./item'); -const template = require('./main.ejs'); - -const TableBody = Mn.CollectionView.extend({ - tagName: 'tbody', - childView: ItemView -}); - -module.exports = Mn.View.extend({ - tagName: 'table', - className: 'table table-hover table-outline table-vcenter card-table', - template: template, - - regions: { - body: { - el: 'tbody', - replaceElement: true - } - }, - - onRender: function () { - this.showChildView('body', new TableBody({ - collection: this.collection - })); - } -}); diff --git a/frontend/js/app/users/main.ejs b/frontend/js/app/users/main.ejs deleted file mode 100644 index 8f0d3aaf..00000000 --- a/frontend/js/app/users/main.ejs +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
-

<%- i18n('users', 'title') %>

- -
-
-
-
-
- -
-
- -
-
diff --git a/frontend/js/app/users/main.js b/frontend/js/app/users/main.js deleted file mode 100644 index 95d42c61..00000000 --- a/frontend/js/app/users/main.js +++ /dev/null @@ -1,55 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../main'); -const UserModel = require('../../models/user'); -const ListView = require('./list/main'); -const ErrorView = require('../error/main'); -const template = require('./main.ejs'); - -module.exports = Mn.View.extend({ - id: 'users', - template: template, - - ui: { - list_region: '.list-region', - add: '.add-item', - dimmer: '.dimmer' - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showUserForm(new UserModel.Model()); - } - }, - - onRender: function () { - let view = this; - - App.Api.Users.getAll(['permissions']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showChildView('list_region', new ListView({ - collection: new UserModel.Collection(response) - })); - } - }) - .catch(err => { - view.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showUsers(); - } - })); - - console.error(err); - }) - .then(() => { - view.ui.dimmer.removeClass('active'); - }); - } -}); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json deleted file mode 100644 index 2fb4a04f..00000000 --- a/frontend/js/i18n/messages.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "en": { - "str": { - "email-address": "Email address", - "username": "Username", - "password": "Password", - "sign-in": "Sign in", - "sign-out": "Sign out", - "try-again": "Try again", - "name": "Name", - "email": "Email", - "roles": "Roles", - "created-on": "Created: {date}", - "save": "Save", - "cancel": "Cancel", - "close": "Close", - "enable": "Enable", - "disable": "Disable", - "sure": "Yes I'm Sure", - "disabled": "Disabled", - "choose-file": "Choose file", - "source": "Source", - "destination": "Destination", - "ssl": "SSL", - "access": "Access", - "public": "Public", - "edit": "Edit", - "delete": "Delete", - "logs": "Logs", - "status": "Status", - "online": "Online", - "offline": "Offline", - "unknown": "Unknown", - "expires": "Expires", - "value": "Value", - "please-wait": "Please wait...", - "all": "All", - "any": "Any" - }, - "login": { - "title": "Login to your account" - }, - "main": { - "app": "Nginx Proxy Manager", - "version": "v{version}", - "welcome": "Welcome to Nginx Proxy Manager", - "logged-in": "You are logged in as {name}", - "unknown-error": "Error loading stuff. Please reload the app.", - "unknown-user": "Unknown User", - "sign-in-as": "Sign back in as {name}" - }, - "roles": { - "title": "Roles", - "admin": "Administrator", - "user": "Apache Helicopter" - }, - "menu": { - "dashboard": "Dashboard", - "hosts": "Hosts" - }, - "footer": { - "fork-me": "Fork me on Github", - "copy": "© 2019 jc21.com.", - "theme": "Theme by Tabler" - }, - "dashboard": { - "title": "Hi {name}" - }, - "all-hosts": { - "empty-subtitle": "{manage, select, true{Why don't you create one?} other{And you don't have permission to create one.}}", - "details": "Details", - "enable-ssl": "Enable SSL", - "force-ssl": "Force SSL", - "http2-support": "HTTP/2 Support", - "domain-names": "Domain Names", - "cert-provider": "Certificate Provider", - "block-exploits": "Block Common Exploits", - "caching-enabled": "Cache Assets", - "ssl-certificate": "SSL Certificate", - "none": "None", - "new-cert": "Request a new SSL Certificate", - "with-le": "with Let's Encrypt", - "no-ssl": "This host will not use HTTPS", - "advanced": "Advanced", - "advanced-warning": "Enter your custom Nginx configuration here at your own risk!", - "advanced-config": "Custom Nginx Configuration", - "hsts-enabled": "HSTS Enabled", - "hsts-subdomains": "HSTS Subdomains", - "locations": "Custom locations" - }, - "locations": { - "new_location": "Add location", - "path": "/path", - "location_label": "Define location", - "delete": "Delete" - }, - "ssl": { - "letsencrypt": "Let's Encrypt", - "other": "Custom", - "none": "HTTP only", - "letsencrypt-email": "Email Address for Let's Encrypt", - "letsencrypt-agree": "I Agree to the Let's Encrypt Terms of Service", - "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", - "hosts-warning": "These domains must be already configured to point to this installation", - "no-wildcard-without-dns": "Cannot request Let's Encrypt Certificate for wildcard domains when not using DNS challenge", - "dns-challenge": "Use a DNS Challenge", - "certbot-warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.", - "dns-provider": "DNS Provider", - "please-choose": "Please Choose...", - "credentials-file-content": "Credentials File Content", - "credentials-file-content-info": "This plugin requires a configuration file containing an API token or other credentials to your provider", - "stored-as-plaintext-info": "This data will be stored as plaintext in the database and in a file!", - "propagation-seconds": "Propagation Seconds", - "propagation-seconds-info": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.", - "processing-info": "Processing... This might take a few minutes.", - "passphrase-protection-support-info": "Key files protected with a passphrase are not supported." - }, - "proxy-hosts": { - "title": "Proxy Hosts", - "empty": "There are no Proxy Hosts", - "add": "Add Proxy Host", - "form-title": "{id, select, undefined{New} other{Edit}} Proxy Host", - "forward-scheme": "Scheme", - "forward-host": "Forward Hostname / IP", - "forward-port": "Forward Port", - "delete": "Delete Proxy Host", - "delete-confirm": "Are you sure you want to delete the Proxy host for: {domains}?", - "help-title": "What is a Proxy Host?", - "help-content": "A Proxy Host is the incoming endpoint for a web service that you want to forward.\nIt provides optional SSL termination for your service that might not have SSL support built in.\nProxy Hosts are the most common use for the Nginx Proxy Manager.", - "access-list": "Access List", - "allow-websocket-upgrade": "Websockets Support", - "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", - "custom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding" - }, - "redirection-hosts": { - "title": "Redirection Hosts", - "empty": "There are no Redirection Hosts", - "add": "Add Redirection Host", - "form-title": "{id, select, undefined{New} other{Edit}} Redirection Host", - "forward-scheme": "Scheme", - "forward-http-status-code": "HTTP Code", - "forward-domain": "Forward Domain", - "preserve-path": "Preserve Path", - "delete": "Delete Proxy Host", - "delete-confirm": "Are you sure you want to delete the Redirection host for: {domains}?", - "help-title": "What is a Redirection Host?", - "help-content": "A Redirection Host will redirect requests from the incoming domain and push the viewer to another domain.\nThe most common reason to use this type of host is when your website changes domains but you still have search engine or referrer links pointing to the old domain." - }, - "dead-hosts": { - "title": "404 Hosts", - "empty": "There are no 404 Hosts", - "add": "Add 404 Host", - "form-title": "{id, select, undefined{New} other{Edit}} 404 Host", - "delete": "Delete 404 Host", - "delete-confirm": "Are you sure you want to delete this 404 Host?", - "help-title": "What is a 404 Host?", - "help-content": "A 404 Host is simply a host setup that shows a 404 page.\nThis can be useful when your domain is listed in search engines and you want to provide a nicer error page or specifically to tell the search indexers that the domain pages no longer exist.\nAnother benefit of having this host is to track the logs for hits to it and view the referrers." - }, - "streams": { - "title": "Streams", - "empty": "There are no Streams", - "add": "Add Stream", - "form-title": "{id, select, undefined{New} other{Edit}} Stream", - "incoming-port": "Incoming Port", - "forward-ip": "Forward IP", - "forwarding-port": "Forward Port", - "tcp-forwarding": "TCP Forwarding", - "udp-forwarding": "UDP Forwarding", - "forward-type-error": "At least one type of protocol must be enabled", - "protocol": "Protocol", - "tcp": "TCP", - "udp": "UDP", - "delete": "Delete Stream", - "delete-confirm": "Are you sure you want to delete this Stream?", - "help-title": "What is a Stream?", - "help-content": "A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP traffic directly to another computer on the network.\nIf you're running game servers, FTP or SSH servers this can come in handy." - }, - "certificates": { - "title": "SSL Certificates", - "empty": "There are no SSL Certificates", - "add": "Add SSL Certificate", - "form-title": "Add {provider, select, letsencrypt{Let's Encrypt} other{Custom}} Certificate", - "delete": "Delete SSL Certificate", - "delete-confirm": "Are you sure you want to delete this SSL Certificate? Any hosts using it will need to be updated later.", - "help-title": "SSL Certificates", - "help-content": "SSL certificates (correctly known as TLS Certificates) are a form of encryption key which allows your site to be encrypted for the end user.\nNPM uses a service called Let's Encrypt to issue SSL certificates for free.\nIf you have any sort of personal information, passwords, or sensitive data behind NPM, it's probably a good idea to use a certificate.\nNPM also supports DNS authentication for if you're not running your site facing the internet, or if you just want a wildcard certificate.", - "other-certificate": "Certificate", - "other-certificate-key": "Certificate Key", - "other-intermediate-certificate": "Intermediate Certificate", - "force-renew": "Renew Now", - "renew-title": "Renew Let'sEncrypt Certificate" - }, - "access-lists": { - "title": "Access Lists", - "empty": "There are no Access Lists", - "add": "Add Access List", - "form-title": "{id, select, undefined{New} other{Edit}} Access List", - "delete": "Delete Access List", - "delete-confirm": "Are you sure you want to delete this access list?", - "public": "Publicly Accessible", - "public-sub": "No Access Restrictions", - "help-title": "What is an Access List?", - "help-content": "Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication.\nYou can configure multiple client rules, usernames and passwords for a single Access List and then apply that to a Proxy Host.\nThis is most useful for forwarded web services that do not have authentication mechanisms built in or that you want to protect from access by unknown clients.", - "item-count": "{count} {count, select, 1{User} other{Users}}", - "client-count": "{count} {count, select, 1{Rule} other{Rules}}", - "proxy-host-count": "{count} {count, select, 1{Proxy Host} other{Proxy Hosts}}", - "delete-has-hosts": "This Access List is associated with {count} Proxy Hosts. They will become publicly available upon deletion.", - "details": "Details", - "authorization": "Authorization", - "access": "Access", - "satisfy": "Satisfy", - "satisfy-any": "Satisfy Any", - "pass-auth": "Pass Auth to Host", - "access-add": "Add", - "auth-add": "Add" - }, - "users": { - "title": "Users", - "default_error": "Default email address must be changed", - "add": "Add User", - "nickname": "Nickname", - "full-name": "Full Name", - "edit-details": "Edit Details", - "change-password": "Change Password", - "edit-permissions": "Edit Permissions", - "sign-in-as": "Sign in as User", - "form-title": "{id, select, undefined{New} other{Edit}} User", - "delete": "Delete {name, select, undefined{User} other{{name}}}", - "delete-confirm": "Are you sure you want to delete {name}?", - "password-title": "Change Password{self, select, false{ for {name}} other{}}", - "current-password": "Current Password", - "new-password": "New Password", - "confirm-password": "Confirm Password", - "permissions-title": "Permissions for {name}", - "admin-perms": "This user is an Administrator and some items cannot be altered", - "perms-visibility": "Item Visibility", - "perms-visibility-user": "Created Items Only", - "perms-visibility-all": "All Items", - "perm-manage": "Manage", - "perm-view": "View Only", - "perm-hidden": "Hidden" - }, - "audit-log": { - "title": "Audit Log", - "empty": "There are no logs.", - "empty-subtitle": "As soon as you or another user changes something, history of those events will show up here.", - "proxy-host": "Proxy Host", - "redirection-host": "Redirection Host", - "dead-host": "404 Host", - "stream": "Stream", - "user": "User", - "certificate": "Certificate", - "access-list": "Access List", - "created": "Created {name}", - "updated": "Updated {name}", - "deleted": "Deleted {name}", - "enabled": "Enabled {name}", - "disabled": "Disabled {name}", - "renewed": "Renewed {name}", - "meta-title": "Details for Event", - "view-meta": "View Details", - "date": "Date" - }, - "settings": { - "title": "Settings", - "default-site": "Default Site", - "default-site-congratulations": "Congratulations Page", - "default-site-404": "404 Page", - "default-site-html": "Custom Page", - "default-site-redirect": "Redirect" - } - } -} diff --git a/frontend/js/index.js b/frontend/js/index.js deleted file mode 100644 index bfaa0175..00000000 --- a/frontend/js/index.js +++ /dev/null @@ -1,112 +0,0 @@ -// This has to exist here so that Webpack picks it up -import '../scss/styles.scss'; - -window.tabler = { - colors: { - 'blue': '#467fcf', - 'blue-darkest': '#0e1929', - 'blue-darker': '#1c3353', - 'blue-dark': '#3866a6', - 'blue-light': '#7ea5dd', - 'blue-lighter': '#c8d9f1', - 'blue-lightest': '#edf2fa', - 'azure': '#45aaf2', - 'azure-darkest': '#0e2230', - 'azure-darker': '#1c4461', - 'azure-dark': '#3788c2', - 'azure-light': '#7dc4f6', - 'azure-lighter': '#c7e6fb', - 'azure-lightest': '#ecf7fe', - 'indigo': '#6574cd', - 'indigo-darkest': '#141729', - 'indigo-darker': '#282e52', - 'indigo-dark': '#515da4', - 'indigo-light': '#939edc', - 'indigo-lighter': '#d1d5f0', - 'indigo-lightest': '#f0f1fa', - 'purple': '#a55eea', - 'purple-darkest': '#21132f', - 'purple-darker': '#42265e', - 'purple-dark': '#844bbb', - 'purple-light': '#c08ef0', - 'purple-lighter': '#e4cff9', - 'purple-lightest': '#f6effd', - 'pink': '#f66d9b', - 'pink-darkest': '#31161f', - 'pink-darker': '#622c3e', - 'pink-dark': '#c5577c', - 'pink-light': '#f999b9', - 'pink-lighter': '#fcd3e1', - 'pink-lightest': '#fef0f5', - 'red': '#e74c3c', - 'red-darkest': '#2e0f0c', - 'red-darker': '#5c1e18', - 'red-dark': '#b93d30', - 'red-light': '#ee8277', - 'red-lighter': '#f8c9c5', - 'red-lightest': '#fdedec', - 'orange': '#fd9644', - 'orange-darkest': '#331e0e', - 'orange-darker': '#653c1b', - 'orange-dark': '#ca7836', - 'orange-light': '#feb67c', - 'orange-lighter': '#fee0c7', - 'orange-lightest': '#fff5ec', - 'yellow': '#f1c40f', - 'yellow-darkest': '#302703', - 'yellow-darker': '#604e06', - 'yellow-dark': '#c19d0c', - 'yellow-light': '#f5d657', - 'yellow-lighter': '#fbedb7', - 'yellow-lightest': '#fef9e7', - 'lime': '#7bd235', - 'lime-darkest': '#192a0b', - 'lime-darker': '#315415', - 'lime-dark': '#62a82a', - 'lime-light': '#a3e072', - 'lime-lighter': '#d7f2c2', - 'lime-lightest': '#f2fbeb', - 'green': '#5eba00', - 'green-darkest': '#132500', - 'green-darker': '#264a00', - 'green-dark': '#4b9500', - 'green-light': '#8ecf4d', - 'green-lighter': '#cfeab3', - 'green-lightest': '#eff8e6', - 'teal': '#2bcbba', - 'teal-darkest': '#092925', - 'teal-darker': '#11514a', - 'teal-dark': '#22a295', - 'teal-light': '#6bdbcf', - 'teal-lighter': '#bfefea', - 'teal-lightest': '#eafaf8', - 'cyan': '#17a2b8', - 'cyan-darkest': '#052025', - 'cyan-darker': '#09414a', - 'cyan-dark': '#128293', - 'cyan-light': '#5dbecd', - 'cyan-lighter': '#b9e3ea', - 'cyan-lightest': '#e8f6f8', - 'gray': '#868e96', - 'gray-darkest': '#1b1c1e', - 'gray-darker': '#36393c', - 'gray-light': '#aab0b6', - 'gray-lighter': '#dbdde0', - 'gray-lightest': '#f3f4f5', - 'gray-dark': '#343a40', - 'gray-dark-darkest': '#0a0c0d', - 'gray-dark-darker': '#15171a', - 'gray-dark-dark': '#2a2e33', - 'gray-dark-light': '#717579', - 'gray-dark-lighter': '#c2c4c6', - 'gray-dark-lightest': '#ebebec' - } -}; - -require('tabler-core'); - -const App = require('./app/main'); - -$(document).ready(() => { - App.start(); -}); diff --git a/frontend/js/lib/helpers.js b/frontend/js/lib/helpers.js deleted file mode 100644 index 21ce7424..00000000 --- a/frontend/js/lib/helpers.js +++ /dev/null @@ -1,26 +0,0 @@ -const numeral = require('numeral'); -const moment = require('moment'); - -module.exports = { - - /** - * @param {Integer} number - * @returns {String} - */ - niceNumber: function (number) { - return numeral(number).format('0,0'); - }, - - /** - * @param {String|Number} date - * @param {String} format - * @returns {String} - */ - formatDbDate: function (date, format) { - if (typeof date === 'number') { - return moment.unix(date).format(format); - } - - return moment(date).format(format); - } -}; diff --git a/frontend/js/lib/marionette.js b/frontend/js/lib/marionette.js deleted file mode 100644 index c88368f8..00000000 --- a/frontend/js/lib/marionette.js +++ /dev/null @@ -1,15 +0,0 @@ -const _ = require('underscore'); -const Mn = require('backbone.marionette'); -const i18n = require('../app/i18n'); -const Helpers = require('./helpers'); -const TemplateCache = require('marionette.templatecache'); - -Mn.setRenderer(function (template, data, view) { - data = _.clone(data); - data.i18n = i18n; - data.formatDbDate = Helpers.formatDbDate; - - return TemplateCache.default.render.call(this, template, data, view); -}); - -module.exports = Mn; diff --git a/frontend/js/login.js b/frontend/js/login.js deleted file mode 100644 index 0094e2a2..00000000 --- a/frontend/js/login.js +++ /dev/null @@ -1,5 +0,0 @@ -const App = require('./login/main'); - -$(document).ready(() => { - App.start(); -}); diff --git a/frontend/js/login/main.js b/frontend/js/login/main.js deleted file mode 100644 index 03fdc7e5..00000000 --- a/frontend/js/login/main.js +++ /dev/null @@ -1,14 +0,0 @@ -const Mn = require('backbone.marionette'); -const LoginView = require('./ui/login'); - -const App = Mn.Application.extend({ - region: '#login', - UI: null, - - onStart: function (/*app, options*/) { - this.getRegion().show(new LoginView()); - } -}); - -const app = new App(); -module.exports = app; diff --git a/frontend/js/login/ui/login.ejs b/frontend/js/login/ui/login.ejs deleted file mode 100644 index b6f52b7a..00000000 --- a/frontend/js/login/ui/login.ejs +++ /dev/null @@ -1,37 +0,0 @@ -
-
- -
-
diff --git a/frontend/js/login/ui/login.js b/frontend/js/login/ui/login.js deleted file mode 100644 index 757eb4e3..00000000 --- a/frontend/js/login/ui/login.js +++ /dev/null @@ -1,42 +0,0 @@ -const $ = require('jquery'); -const Mn = require('backbone.marionette'); -const template = require('./login.ejs'); -const Api = require('../../app/api'); -const i18n = require('../../app/i18n'); - -module.exports = Mn.View.extend({ - template: template, - className: 'page-single', - - ui: { - form: 'form', - identity: 'input[name="identity"]', - secret: 'input[name="secret"]', - error: '.secret-error', - button: 'button' - }, - - events: { - 'submit @ui.form': function (e) { - e.preventDefault(); - this.ui.button.addClass('btn-loading').prop('disabled', true); - this.ui.error.hide(); - - Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true) - .then(() => { - window.location = '/'; - }) - .catch(err => { - this.ui.error.text(err.message).show(); - this.ui.button.removeClass('btn-loading').prop('disabled', false); - }); - } - }, - - templateContext: { - i18n: i18n, - getVersion: function () { - return $('#login').data('version'); - } - } -}); diff --git a/frontend/js/models/access-list.js b/frontend/js/models/access-list.js deleted file mode 100644 index 0c2c4abe..00000000 --- a/frontend/js/models/access-list.js +++ /dev/null @@ -1,25 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - name: '', - items: [], - clients: [], - // The following are expansions: - owner: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/audit-log.js b/frontend/js/models/audit-log.js deleted file mode 100644 index c929a0bd..00000000 --- a/frontend/js/models/audit-log.js +++ /dev/null @@ -1,18 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - name: '' - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/certificate.js b/frontend/js/models/certificate.js deleted file mode 100644 index c7d0b2d9..00000000 --- a/frontend/js/models/certificate.js +++ /dev/null @@ -1,38 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - provider: '', - nice_name: '', - domain_names: [], - expires_on: null, - meta: {}, - // The following are expansions: - owner: null, - proxy_hosts: [], - redirection_hosts: [], - dead_hosts: [] - }; - }, - - /** - * @returns {Boolean} - */ - hasSslFiles: function () { - let meta = this.get('meta'); - return typeof meta['certificate'] !== 'undefined' && meta['certificate'] && typeof meta['certificate_key'] !== 'undefined' && meta['certificate_key']; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/dead-host.js b/frontend/js/models/dead-host.js deleted file mode 100644 index 98ceef29..00000000 --- a/frontend/js/models/dead-host.js +++ /dev/null @@ -1,32 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - certificate_id: 0, - ssl_forced: false, - http2_support: false, - hsts_enabled: false, - hsts_subdomains: false, - enabled: true, - meta: {}, - advanced_config: '', - // The following are expansions: - owner: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/proxy-host-location.js b/frontend/js/models/proxy-host-location.js deleted file mode 100644 index 2a35059f..00000000 --- a/frontend/js/models/proxy-host-location.js +++ /dev/null @@ -1,35 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function() { - return { - opened: false, - path: '', - advanced_config: '', - forward_scheme: 'http', - forward_host: '', - forward_port: '80' - } - }, - - toJSON() { - const r = Object.assign({}, this.attributes); - delete r.opened; - return r; - }, - - toggleVisibility: function () { - this.save({ - opened: !this.get('opened') - }); - } -}) - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model - }) -} \ No newline at end of file diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js deleted file mode 100644 index b82d09fe..00000000 --- a/frontend/js/models/proxy-host.js +++ /dev/null @@ -1,40 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - forward_scheme: 'http', - forward_host: '', - forward_port: null, - access_list_id: 0, - certificate_id: 0, - ssl_forced: false, - hsts_enabled: false, - hsts_subdomains: false, - caching_enabled: false, - allow_websocket_upgrade: false, - block_exploits: false, - http2_support: false, - advanced_config: '', - enabled: true, - meta: {}, - // The following are expansions: - owner: null, - access_list: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/redirection-host.js b/frontend/js/models/redirection-host.js deleted file mode 100644 index 1d0b0de2..00000000 --- a/frontend/js/models/redirection-host.js +++ /dev/null @@ -1,37 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - domain_names: [], - forward_http_code: 0, - forward_scheme: null, - forward_domain_name: '', - preserve_path: true, - certificate_id: 0, - ssl_forced: false, - hsts_enabled: false, - hsts_subdomains: false, - block_exploits: false, - http2_support: false, - advanced_config: '', - enabled: true, - meta: {}, - // The following are expansions: - owner: null, - certificate: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/setting.js b/frontend/js/models/setting.js deleted file mode 100644 index c70a4e9c..00000000 --- a/frontend/js/models/setting.js +++ /dev/null @@ -1,22 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - name: '', - description: '', - value: null, - meta: [] - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/stream.js b/frontend/js/models/stream.js deleted file mode 100644 index e4693549..00000000 --- a/frontend/js/models/stream.js +++ /dev/null @@ -1,29 +0,0 @@ -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - created_on: null, - modified_on: null, - incoming_port: null, - forward_ip: null, - forwarding_port: null, - tcp_forwarding: true, - udp_forwarding: false, - enabled: true, - meta: {}, - // The following are expansions: - owner: null - }; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/js/models/user.js b/frontend/js/models/user.js deleted file mode 100644 index a8e4ed9e..00000000 --- a/frontend/js/models/user.js +++ /dev/null @@ -1,54 +0,0 @@ -const _ = require('underscore'); -const Backbone = require('backbone'); - -const model = Backbone.Model.extend({ - idAttribute: 'id', - - defaults: function () { - return { - id: undefined, - name: '', - nickname: '', - email: '', - is_disabled: false, - roles: [], - permissions: null - }; - }, - - /** - * @returns {Boolean} - */ - isAdmin: function () { - return _.indexOf(this.get('roles'), 'admin') !== -1; - }, - - /** - * Checks if the perm has either `view` or `manage` value - * - * @param {String} item - * @returns {Boolean} - */ - canView: function (item) { - let permissions = this.get('permissions'); - return permissions !== null && typeof permissions[item] !== 'undefined' && ['view', 'manage'].indexOf(permissions[item]) !== -1; - }, - - /** - * Checks if the perm has `manage` value - * - * @param {String} item - * @returns {Boolean} - */ - canManage: function (item) { - let permissions = this.get('permissions'); - return permissions !== null && typeof permissions[item] !== 'undefined' && permissions[item] === 'manage'; - } -}); - -module.exports = { - Model: model, - Collection: Backbone.Collection.extend({ - model: model - }) -}; diff --git a/frontend/package.json b/frontend/package.json index daa48f3c..979d6a30 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,48 +1,91 @@ { - "name": "nginx-proxy-manager", - "version": "0.0.0", - "description": "A beautiful interface for creating Nginx endpoints", - "main": "js/index.js", - "devDependencies": { - "@babel/core": "^7.9.0", - "babel-core": "^6.26.3", - "babel-loader": "^8.1.0", - "babel-minify-webpack-plugin": "^0.3.1", - "babel-preset-env": "^1.7.0", - "backbone": "^1.4.0", - "backbone.marionette": "^4.1.2", - "copy-webpack-plugin": "^5.1.1", - "css-loader": "^3.5.0", - "ejs-lint": "^1.0.1", - "ejs-loader": "^0.3.6", - "ejs-webpack-loader": "^2.2.2", - "file-loader": "^6.0.0", - "html-webpack-plugin": "^4.0.4", - "imports-loader": "^0.8.0", - "jquery": "^3.5.0", - "jquery-mask-plugin": "^1.14.16", - "jquery-serializejson": "^2.9.0", - "marionette.approuter": "^1.0.2", - "marionette.templatecache": "^1.0.0", - "messageformat": "^2.3.0", - "messageformat-loader": "^0.8.1", - "mini-css-extract-plugin": "^0.9.0", - "moment": "^2.24.0", - "node-sass": "^4.13.1", - "nodemon": "^2.0.2", - "numeral": "^2.0.6", - "sass-loader": "^8.0.2", - "style-loader": "^1.1.3", - "tabler-ui": "git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813", - "underscore": "^1.12.1", - "webpack": "^4.42.1", - "webpack-cli": "^3.3.11", - "webpack-visualizer-plugin": "^0.1.11" - }, - "scripts": { - "build": "webpack --mode production", - "watch": "webpack --watch --mode development" - }, - "author": "Jamie Curnow ", - "license": "MIT" + "name": "nginxproxymanager", + "version": "1.0.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "5.12.0", + "@testing-library/react": "11.2.7", + "@types/humps": "^2.0.0", + "@types/jest": "26.0.23", + "@types/lodash": "4.14.169", + "@types/node": "15.3.0", + "@types/react": "17.0.5", + "@types/react-dom": "17.0.5", + "@types/react-router-dom": "5.1.7", + "@types/styled-components": "5.1.9", + "@typescript-eslint/eslint-plugin": "^4.23.0", + "@typescript-eslint/parser": "^4.23.0", + "babel-eslint": "^10.1.0", + "date-fns": "2.21.3", + "eslint": "^7.26.0", + "eslint-config-prettier": "^8.3.0", + "eslint-config-react-app": "^6.0.0", + "eslint-loader": "^4.0.2", + "eslint-plugin-flowtype": "^5.7.2", + "eslint-plugin-import": "^2.23.2", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react-hooks": "^4.2.0", + "humps": "^2.0.1", + "jest-date-mock": "1.0.8", + "jest-fetch-mock": "3.0.3", + "jest-junit": "^12.0.0", + "jest-localstorage-mock": "2.4.12", + "jest-runner-eslint": "0.10.0", + "lodash": "4.17.21", + "moment": "2.29.1", + "node-sass": "^5.0.0", + "prettier": "2.3.0", + "query-string": "7.0.0", + "react": "17.0.2", + "react-async": "10.0.1", + "react-dom": "17.0.2", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.3", + "rooks": "5.0.2", + "styled-components": "5.3.0", + "tabler-react": "^2.0.0-alpha.1", + "typescript": "^4.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "lint": "yarn jest --config jest.eslint.js --watch", + "lint:ci": "yarn lint --watchAll=false", + "test": "react-scripts test", + "test:coverage": "yarn test --coverage --watchAll=false", + "test:ci": "yarn test:coverage", + "eject": "react-scripts eject", + "prettier": "prettier \"**/*.+(js|json|yml|css|ts|tsx)\"", + "format": "yarn prettier -- --write", + "lint:fix": "eslint --fix --ext .ts --ext .tsx ." + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "jest": { + "globalSetup": "/globalSetup.js", + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/testUtils/*" + ], + "coverageThreshold": { + "global": { + "branches": 1, + "functions": 1, + "lines": 1, + "statements": 1 + } + } + } } diff --git a/frontend/app-images/default-avatar.jpg b/frontend/public/images/default-avatar.jpg similarity index 100% rename from frontend/app-images/default-avatar.jpg rename to frontend/public/images/default-avatar.jpg diff --git a/frontend/app-images/favicons/android-chrome-192x192.png b/frontend/public/images/favicon/android-chrome-192x192.png similarity index 100% rename from frontend/app-images/favicons/android-chrome-192x192.png rename to frontend/public/images/favicon/android-chrome-192x192.png diff --git a/frontend/app-images/favicons/android-chrome-512x512.png b/frontend/public/images/favicon/android-chrome-512x512.png similarity index 100% rename from frontend/app-images/favicons/android-chrome-512x512.png rename to frontend/public/images/favicon/android-chrome-512x512.png diff --git a/frontend/app-images/favicons/apple-touch-icon.png b/frontend/public/images/favicon/apple-touch-icon.png similarity index 100% rename from frontend/app-images/favicons/apple-touch-icon.png rename to frontend/public/images/favicon/apple-touch-icon.png diff --git a/frontend/app-images/favicons/browserconfig.xml b/frontend/public/images/favicon/browserconfig.xml similarity index 100% rename from frontend/app-images/favicons/browserconfig.xml rename to frontend/public/images/favicon/browserconfig.xml diff --git a/frontend/app-images/favicons/favicon-16x16.png b/frontend/public/images/favicon/favicon-16x16.png similarity index 100% rename from frontend/app-images/favicons/favicon-16x16.png rename to frontend/public/images/favicon/favicon-16x16.png diff --git a/frontend/app-images/favicons/favicon-32x32.png b/frontend/public/images/favicon/favicon-32x32.png similarity index 100% rename from frontend/app-images/favicons/favicon-32x32.png rename to frontend/public/images/favicon/favicon-32x32.png diff --git a/frontend/app-images/favicons/favicon.ico b/frontend/public/images/favicon/favicon.ico similarity index 100% rename from frontend/app-images/favicons/favicon.ico rename to frontend/public/images/favicon/favicon.ico diff --git a/frontend/app-images/favicons/mstile-150x150.png b/frontend/public/images/favicon/mstile-150x150.png similarity index 100% rename from frontend/app-images/favicons/mstile-150x150.png rename to frontend/public/images/favicon/mstile-150x150.png diff --git a/frontend/app-images/favicons/safari-pinned-tab.svg b/frontend/public/images/favicon/safari-pinned-tab.svg similarity index 100% rename from frontend/app-images/favicons/safari-pinned-tab.svg rename to frontend/public/images/favicon/safari-pinned-tab.svg diff --git a/frontend/app-images/favicons/site.webmanifest b/frontend/public/images/favicon/site.webmanifest similarity index 100% rename from frontend/app-images/favicons/site.webmanifest rename to frontend/public/images/favicon/site.webmanifest diff --git a/frontend/app-images/logo-256.png b/frontend/public/images/logo-256.png similarity index 100% rename from frontend/app-images/logo-256.png rename to frontend/public/images/logo-256.png diff --git a/frontend/public/images/logo-bold-horizontal-grey.svg b/frontend/public/images/logo-bold-horizontal-grey.svg new file mode 100644 index 00000000..c87396f6 --- /dev/null +++ b/frontend/public/images/logo-bold-horizontal-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/logo-text-horizontal-grey.png b/frontend/public/images/logo-text-horizontal-grey.png new file mode 100644 index 00000000..057a83d1 Binary files /dev/null and b/frontend/public/images/logo-text-horizontal-grey.png differ diff --git a/frontend/app-images/logo-text-vertical-grey.png b/frontend/public/images/logo-text-vertical-grey.png similarity index 100% rename from frontend/app-images/logo-text-vertical-grey.png rename to frontend/public/images/logo-text-vertical-grey.png diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 00000000..b27987b8 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,59 @@ + + + + + + Nginx Proxy Manager + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/frontend/scss/custom.scss b/frontend/scss/custom.scss deleted file mode 100644 index 4037dcf6..00000000 --- a/frontend/scss/custom.scss +++ /dev/null @@ -1,42 +0,0 @@ -$primary-color: #2bcbba; - -.loader { - color: $primary-color; -} - -a { - color: $primary-color; -} - -a:hover { - color: darken($primary-color, 10%); -} - -.dropdown-header { - padding-left: 1rem; -} - -.dropdown-item.active, .dropdown-item:active { - background-color: $primary-color; -} - -.custom-switch-input:checked ~ .custom-switch-indicator { - background: $primary-color; -} - -.min-100 { - min-height: 100px; -} - -.card-options .dropdown-menu a:not(.btn) { - margin-left: 0; -} - -.wrap { - display: flex; - flex-wrap: wrap; -} - -.col-login { - max-width: 48rem; -} \ No newline at end of file diff --git a/frontend/scss/fonts.scss b/frontend/scss/fonts.scss deleted file mode 100644 index f0ec1b73..00000000 --- a/frontend/scss/fonts.scss +++ /dev/null @@ -1,39 +0,0 @@ -/* source-sans-pro-regular - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 400; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-italic - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: italic; - font-weight: 400; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-700italic - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: italic; - font-weight: 700; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-700 - latin-ext_latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 700; - src: local(''), - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('../fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} diff --git a/frontend/scss/selectize.scss b/frontend/scss/selectize.scss deleted file mode 100644 index e12d5b69..00000000 --- a/frontend/scss/selectize.scss +++ /dev/null @@ -1,196 +0,0 @@ -.selectize-dropdown-header { - position: relative; - padding: 5px 8px; - background: #f8f8f8; - border-bottom: 1px solid #d0d0d0; - -webkit-border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} - -.selectize-dropdown-header-close { - position: absolute; - top: 50%; - right: 8px; - margin-top: -12px; - font-size: 20px !important; - line-height: 20px; - color: #303030; - opacity: 0.4; -} - -.selectize-dropdown-header-close:hover { - color: #000000; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup { - float: left; - border-top: 0 none; - border-right: 1px solid #f2f2f2; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { - border-right: 0 none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:before { - display: none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup-header { - border-top: 0 none; -} - -.selectize-control.plugin-remove_button [data-value] { - position: relative; - padding-right: 24px !important; -} - -.selectize-control.plugin-remove_button [data-value] .remove { - position: absolute; - top: 0; - right: 0; - bottom: 0; - display: inline-block; - width: 17px; - padding: 2px 0 0 0; - font-size: 12px; - font-weight: bold; - color: inherit; - text-align: center; - text-decoration: none; - vertical-align: middle; - border-left: 1px solid #0073bb; - -webkit-border-radius: 0 2px 2px 0; - -moz-border-radius: 0 2px 2px 0; - border-radius: 0 2px 2px 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-control.plugin-remove_button [data-value] .remove:hover { - background: rgba(0, 0, 0, 0.05); -} - -.selectize-control.plugin-remove_button [data-value].active .remove { - border-left-color: #00578d; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { - background: none; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove { - border-left-color: #aaaaaa; -} - -.selectize-control { - position: relative; -} - -.selectize-dropdown { - font-family: inherit; - font-size: 13px; - -webkit-font-smoothing: inherit; - line-height: 18px; - color: #303030; -} - -.selectize-control.single { - display: inline-block; - cursor: text; - background: #ffffff; -} - -.selectize-dropdown { - position: absolute; - z-index: 10; - margin: -1px 0 0 0; - background: #ffffff; - border: 1px solid #d0d0d0; - border-top: 0 none; - -webkit-border-radius: 0 0 3px 3px; - -moz-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.selectize-dropdown [data-selectable] { - overflow: hidden; - cursor: pointer; -} - -.selectize-dropdown [data-selectable] .highlight { - background: rgba(125, 168, 208, 0.2); - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; -} - -.selectize-dropdown [data-selectable], -.selectize-dropdown .optgroup-header { - padding: 5px 8px; -} - -.selectize-dropdown .optgroup:first-child .optgroup-header { - border-top: 0 none; -} - -.selectize-dropdown .optgroup-header { - color: #303030; - cursor: default; - background: #ffffff; -} - -.selectize-dropdown .active { - color: #495c68; - background-color: #f5fafd; -} - -.selectize-dropdown .active.create { - color: #495c68; -} - -.selectize-dropdown .create { - color: rgba(48, 48, 48, 0.5); -} - -.selectize-dropdown-content { - max-height: 200px; - overflow-x: hidden; - overflow-y: auto; - - .title { - font-weight: bold; - } - - .description { - padding-left: 16px; - } -} - -.selectize-dropdown .optgroup-header { - padding-top: 7px; - font-size: 0.85em; - font-weight: bold; -} - -.selectize-dropdown .optgroup { - border-top: 1px solid #f0f0f0; -} - -.selectize-dropdown .optgroup:first-child { - border-top: 0 none; -} - -.custom-select { - height: auto; -} diff --git a/frontend/scss/styles.scss b/frontend/scss/styles.scss deleted file mode 100644 index 52733097..00000000 --- a/frontend/scss/styles.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import "~tabler-ui/dist/assets/css/dashboard"; -@import "tabler-extra"; -@import "fonts"; -@import "selectize"; -@import "custom"; - -/* Before any JS content is loaded */ -#app > .loader, #login > .loader, .container > .loader { - position: absolute; - left: 49%; - top: 40%; - display: block; -} - -.no-js-warning { - margin-top: 100px; -} diff --git a/frontend/scss/tabler-extra.scss b/frontend/scss/tabler-extra.scss deleted file mode 100644 index 3ddd0ed4..00000000 --- a/frontend/scss/tabler-extra.scss +++ /dev/null @@ -1,170 +0,0 @@ -$teal: #2bcbba; -$yellow: #f1c40f; -$blue: #467fcf; -$pink: #f66d9b; - -.tag { - margin-bottom: .5em; - margin-right: .5em; -} - -.tag.hover-green:hover, .tag.hover-green:active, .tag.hover-green:focus { - background-color: #5eba00; - cursor: pointer; - color: #fff; -} - -.tag.hover-red:hover, .tag.hover-red:active, .tag.hover-red:focus { - background-color: #cd201f; - cursor: pointer; - color: #fff; -} - -/* For Card bodies where I don't want padding */ -.card-body.no-padding { - padding: 0; -} - -/* For some reason this class doesn't have 'display: flex' when it should. https://preview.tabler.io/docs/buttons.html#list-of-buttons */ -.btn-list { - display: flex; -} - -/* Teal Outline Buttons */ -.btn-outline-teal { - color: $teal; - background-color: transparent; - background-image: none; - border-color: $teal; -} - -.btn-outline-teal:hover { - color: #fff; - background-color: $teal; - border-color: $teal; -} - -.btn-outline-teal:not(:disabled):not(.disabled):active, .btn-outline-teal:not(:disabled):not(.disabled).active, .show > .btn-outline-teal.dropdown-toggle { - color: #fff; - background-color: $teal; - border-color: $teal; -} - -.tag.hover-teal:hover, .tag.hover-teal:active, .tag.hover-teal:focus { - background-color: $teal; - color: #fff; - cursor: pointer; -} - -/* Yellow Outline Buttons */ -.btn-outline-yellow { - color: $yellow; - background-color: transparent; - background-image: none; - border-color: $yellow; -} - -.btn-outline-yellow:hover { - color: #fff; - background-color: $yellow; - border-color: $yellow; -} - -.btn-outline-yellow:not(:disabled):not(.disabled):active, .btn-outline-yellow:not(:disabled):not(.disabled).active, .show > .btn-outline-yellow.dropdown-toggle { - color: #fff; - background-color: $yellow; - border-color: $yellow; -} - -.tag.hover-yellow:hover, .tag.hover-yellow:active, .tag.hover-yellow:focus { - background-color: $yellow; - cursor: pointer; - color: #fff; -} - -/* Blue Outline Buttons */ -.btn-outline-blue { - color: $blue; - background-color: transparent; - background-image: none; - border-color: $blue; -} - -.btn-outline-blue:hover { - color: #fff; - background-color: $blue; - border-color: $blue; -} - -.btn-outline-blue:not(:disabled):not(.disabled):active, .btn-outline-blue:not(:disabled):not(.disabled).active, .show > .btn-outline-blue.dropdown-toggle { - color: #fff; - background-color: $blue; - border-color: $blue; -} - -.tag.hover-blue:hover, .tag.hover-blue:active, .tag.hover-blue:focus { - background-color: $blue; - cursor: pointer; - color: #fff; -} - -/* Pink Outline Buttons */ -.btn-outline-pink { - color: $pink; - background-color: transparent; - background-image: none; - border-color: $pink; -} - -.btn-outline-pink:hover { - color: #fff; - background-color: $pink; - border-color: $pink; -} - -.btn-outline-pink:not(:disabled):not(.disabled):active, .btn-outline-pink:not(:disabled):not(.disabled).active, .show > .btn-outline-pink.dropdown-toggle { - color: #fff; - background-color: $pink; - border-color: $pink; -} - -.tag.hover-pink:hover, .tag.hover-pink:active, .tag.hover-pink:focus { - background-color: $pink; - cursor: pointer; -} - -/* dimmer */ - -.dimmer .loader { - margin-top: 50px; -} - -/* modal tabs */ - -.modal-body.has-tabs { - padding: 0; - - .nav-tabs { - margin: 0; - } - - .tab-content { - padding: 1rem; - } -} - -/* modal wide */ - -@media (min-width: 576px) { - .modal-dialog.wide { - max-width: 700px; - margin: 1.75rem auto; - } -} - - -/* Form mod */ - -textarea.form-control.text-monospace { - font-size: 12px; -} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 00000000..1aad9cc7 --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; + +import * as ReactDOM from "react-dom"; + +import App from "./App"; + +it("renders without crashing", () => { + const div = document.createElement("div"); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..ad9a2908 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import Router from "components/Router"; +import { AuthProvider, HealthProvider } from "context"; + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/frontend/src/api/npm/base.ts b/frontend/src/api/npm/base.ts new file mode 100644 index 00000000..2d421463 --- /dev/null +++ b/frontend/src/api/npm/base.ts @@ -0,0 +1,89 @@ +import { camelizeKeys, decamelizeKeys } from "humps"; +import AuthStore from "modules/AuthStore"; +import * as queryString from "query-string"; + +interface BuildUrlArgs { + url: string; + params?: queryString.StringifiableRecord; +} + +function buildUrl({ url, params }: BuildUrlArgs) { + const endpoint = url.replace(/^\/|\/$/g, ""); + const apiParams = params ? `?${queryString.stringify(params)}` : ""; + const apiUrl = `/api/${endpoint}${apiParams}`; + return apiUrl; +} + +function buildAuthHeader(): Record | undefined { + if (AuthStore.token) { + return { Authorization: `Bearer ${AuthStore.token.token}` }; + } + return {}; +} + +function buildBody(data?: Record) { + if (data) { + return JSON.stringify(decamelizeKeys(data)); + } +} + +async function processResponse(response: Response) { + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error.message); + } + return camelizeKeys(payload) as any; +} + +interface GetArgs { + url: string; + params?: queryString.StringifiableRecord; +} + +export async function get( + { url, params }: GetArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url, params }); + const method = "GET"; + const signal = abortController?.signal; + const headers = buildAuthHeader(); + const response = await fetch(apiUrl, { method, headers, signal }); + return processResponse(response); +} + +interface PostArgs { + url: string; + data?: any; +} + +export async function post( + { url, data }: PostArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "POST"; + const headers = { ...buildAuthHeader(), "Content-Type": "application/json" }; + const signal = abortController?.signal; + const body = buildBody(data); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} + +interface PutArgs { + url: string; + data?: any; +} + +export async function put( + { url, data }: PutArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "PUT"; + const headers = { ...buildAuthHeader(), "Content-Type": "application/json" }; + const signal = abortController?.signal; + const body = buildBody(data); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} diff --git a/frontend/src/api/npm/createUser.ts b/frontend/src/api/npm/createUser.ts new file mode 100644 index 00000000..08c7f100 --- /dev/null +++ b/frontend/src/api/npm/createUser.ts @@ -0,0 +1,32 @@ +import * as api from "./base"; +import { UserResponse } from "./responseTypes"; + +interface AuthOptions { + type: string; + secret: string; +} + +interface Options { + payload: { + name: string; + nickname: string; + email: string; + roles: string[]; + isDisabled: boolean; + auth: AuthOptions; + }; +} + +export async function createUser( + { payload }: Options, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/users", + data: payload, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getToken.ts b/frontend/src/api/npm/getToken.ts new file mode 100644 index 00000000..ff7c5557 --- /dev/null +++ b/frontend/src/api/npm/getToken.ts @@ -0,0 +1,24 @@ +import * as api from "./base"; +import { TokenResponse } from "./responseTypes"; + +interface Options { + payload: { + type: string; + identity: string; + secret: string; + }; +} + +export async function getToken( + { payload }: Options, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/tokens", + data: payload, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getUser.ts b/frontend/src/api/npm/getUser.ts new file mode 100644 index 00000000..70230836 --- /dev/null +++ b/frontend/src/api/npm/getUser.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import { UserResponse } from "./responseTypes"; + +export async function getUser( + id: number | string = "me", +): Promise { + const userId = id ? id : "me"; + const { result } = await api.get({ + url: `/users/${userId}`, + }); + return result; +} diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts new file mode 100644 index 00000000..c4ffe25c --- /dev/null +++ b/frontend/src/api/npm/index.ts @@ -0,0 +1,6 @@ +export * from "./createUser"; +export * from "./getToken"; +export * from "./getUser"; +export * from "./refreshToken"; +export * from "./requestHealth"; +export * from "./responseTypes"; diff --git a/frontend/src/api/npm/refreshToken.ts b/frontend/src/api/npm/refreshToken.ts new file mode 100644 index 00000000..de31f401 --- /dev/null +++ b/frontend/src/api/npm/refreshToken.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { TokenResponse } from "./responseTypes"; + +export async function refreshToken( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "/tokens", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/requestHealth.ts b/frontend/src/api/npm/requestHealth.ts new file mode 100644 index 00000000..92a04006 --- /dev/null +++ b/frontend/src/api/npm/requestHealth.ts @@ -0,0 +1,15 @@ +import * as api from "./base"; +import { HealthResponse } from "./responseTypes"; + +// Request function. +export async function requestHealth( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts new file mode 100644 index 00000000..900af8ff --- /dev/null +++ b/frontend/src/api/npm/responseTypes.ts @@ -0,0 +1,33 @@ +export interface HealthResponse { + commit: string; + errorReporting: boolean; + healthy: boolean; + setup: boolean; + version: string; +} + +export interface UserAuthResponse { + id: number; + userId: number; + type: string; + createdOn: number; + updatedOn: number; +} + +export interface TokenResponse { + expires: number; + token: string; +} + +export interface UserResponse { + id: number; + name: string; + nickname: string; + email: string; + createdOn: number; + updatedOn: number; + roles: string[]; + gravatarUrl: string; + isDisabled: boolean; + auth?: UserAuthResponse; +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 00000000..64d81357 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { useHealthState } from "context"; +import styled from "styled-components"; +import { Site } from "tabler-react"; + +const FixedFooterWrapper = styled.div` + position: fixed; + bottom: 0; + width: 100%; +`; + +interface Props { + fixed?: boolean; +} +function Footer({ fixed }: Props) { + const { health } = useHealthState(); + + const footerNav = ( +
+ + User Guide + {" "} + {String.fromCharCode(183)}{" "} + + Changelog + {" "} + {String.fromCharCode(183)}{" "} + + Github + +
+ ); + + const note = + "v" + health.version + " " + String.fromCharCode(183) + " " + health.commit; + + return fixed ? ( + + + + ) : ( + + ); +} + +export { Footer }; diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx new file mode 100644 index 00000000..03b0ffae --- /dev/null +++ b/frontend/src/components/Loading.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import styled from "styled-components"; +import { Loader } from "tabler-react"; + +const Root = styled.div` + text-align: center; +`; + +function Loading() { + return ( + + + + ); +} + +export { Loading }; diff --git a/frontend/src/components/Router.tsx b/frontend/src/components/Router.tsx new file mode 100644 index 00000000..553641d7 --- /dev/null +++ b/frontend/src/components/Router.tsx @@ -0,0 +1,57 @@ +import React, { lazy, Suspense } from "react"; + +import { Loading, SiteWrapper, SinglePage } from "components"; +import { useAuthState, useHealthState, UserProvider } from "context"; +import { BrowserRouter, Switch, Route } from "react-router-dom"; + +const Setup = lazy(() => import("pages/Setup")); +const Dashboard = lazy(() => import("pages/Dashboard")); +const Login = lazy(() => import("pages/Login")); + +function Router() { + const { health } = useHealthState(); + const { authenticated } = useAuthState(); + const Spinner = ( + + + + ); + + if (health.loading) { + return Spinner; + } + + if (health.healthy && !health.setup) { + return ( + + + + ); + } + + if (!authenticated) { + return ( + + + + ); + } + + return ( + + + + + + + + + + + + + + ); +} + +export default Router; diff --git a/frontend/src/components/SinglePage.tsx b/frontend/src/components/SinglePage.tsx new file mode 100644 index 00000000..09f89895 --- /dev/null +++ b/frontend/src/components/SinglePage.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from "react"; + +import { Footer } from "components"; +import styled from "styled-components"; + +const Root = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + min-height: 100%; +`; + +const Wrapper = styled.div` + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: center; +`; + +interface Props { + children?: ReactNode; +} +function SinglePage({ children }: Props) { + return ( + + {children} +